From ff5481e5bb77f38ebdef91dd0ea992657d375dd9 Mon Sep 17 00:00:00 2001 From: Harry Shoff Date: Tue, 13 Jun 2017 08:54:56 -0700 Subject: [PATCH 1/6] [legend] start --- packages/vx-demo/package.json | 4 +- packages/vx-demo/pages/legends.js | 19 ++++++++ packages/vx-legend/.babelrc | 19 ++++++++ packages/vx-legend/Makefile | 1 + packages/vx-legend/package.json | 44 +++++++++++++++++++ packages/vx-legend/src/index.js | 1 + packages/vx-legend/src/labels/quantile.js | 10 +++++ packages/vx-legend/src/legends/Legend.js | 26 +++++++++++ packages/vx-legend/test/Legend.test.js | 7 +++ packages/vx-scale/src/index.js | 3 ++ packages/vx-scale/src/scales/quantile.js | 13 ++++++ packages/vx-scale/src/scales/quantize.js | 19 ++++++++ packages/vx-scale/src/scales/threshold.js | 13 ++++++ packages/vx-scale/test/scaleQuantile.test.js | 19 ++++++++ packages/vx-scale/test/scaleQuantize.test.js | 19 ++++++++ packages/vx-scale/test/scaleThreshold.test.js | 19 ++++++++ 16 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 packages/vx-demo/pages/legends.js create mode 100644 packages/vx-legend/.babelrc create mode 100644 packages/vx-legend/Makefile create mode 100644 packages/vx-legend/package.json create mode 100644 packages/vx-legend/src/index.js create mode 100644 packages/vx-legend/src/labels/quantile.js create mode 100644 packages/vx-legend/src/legends/Legend.js create mode 100644 packages/vx-legend/test/Legend.test.js create mode 100644 packages/vx-scale/src/scales/quantile.js create mode 100644 packages/vx-scale/src/scales/quantize.js create mode 100644 packages/vx-scale/src/scales/threshold.js create mode 100644 packages/vx-scale/test/scaleQuantile.test.js create mode 100644 packages/vx-scale/test/scaleQuantize.test.js create mode 100644 packages/vx-scale/test/scaleThreshold.test.js diff --git a/packages/vx-demo/package.json b/packages/vx-demo/package.json index a1af39304..215d4c933 100644 --- a/packages/vx-demo/package.json +++ b/packages/vx-demo/package.json @@ -27,6 +27,7 @@ "@vx/group": "0.0.120", "@vx/heatmap": "0.0.120", "@vx/hierarchy": "0.0.120", + "@vx/legend": "1.0.0", "@vx/marker": "0.0.120", "@vx/mock-data": "0.0.115", "@vx/pattern": "0.0.120", @@ -37,6 +38,7 @@ "@vx/text": "0.0.120", "classnames": "^2.2.5", "d3-array": "^1.1.1", + "d3-format": "^1.2.0", "d3-hierarchy": "^1.1.4", "d3-shape": "^1.0.6", "d3-time-format": "^2.0.5", @@ -66,4 +68,4 @@ "react-addons-test-utils": "^15.5.1", "regenerator-runtime": "^0.10.5" } -} +} \ No newline at end of file diff --git a/packages/vx-demo/pages/legends.js b/packages/vx-demo/pages/legends.js new file mode 100644 index 000000000..c980c32c5 --- /dev/null +++ b/packages/vx-demo/pages/legends.js @@ -0,0 +1,19 @@ +import { Legend } from '@vx/legend'; +import { scaleQuantize } from '@vx/scale'; +import { format } from 'd3-format'; + +const scale = scaleQuantize({ + domain: [0, 0.15], + range: new Array(9).fill(1).map((v,i) => `q${i}-9`) +}); + +export default () => { + return ( + + + + ); +} \ No newline at end of file diff --git a/packages/vx-legend/.babelrc b/packages/vx-legend/.babelrc new file mode 100644 index 000000000..44157e5b6 --- /dev/null +++ b/packages/vx-legend/.babelrc @@ -0,0 +1,19 @@ +{ + "presets": ["es2015", "react", "stage-0"], + "plugins": [], + "env": { + "development": { + "plugins": [ + ["react-transform", { + "transforms": [{ + "transform": "react-transform-hmr", + "imports": ["react"], + "locals": ["module"] + }] + }], + "transform-runtime", + "transform-decorators-legacy" + ] + } + } +} diff --git a/packages/vx-legend/Makefile b/packages/vx-legend/Makefile new file mode 100644 index 000000000..f7e19ad08 --- /dev/null +++ b/packages/vx-legend/Makefile @@ -0,0 +1 @@ +include node_modules/react-fatigue-dev/Makefile diff --git a/packages/vx-legend/package.json b/packages/vx-legend/package.json new file mode 100644 index 000000000..6961d15ed --- /dev/null +++ b/packages/vx-legend/package.json @@ -0,0 +1,44 @@ +{ + "name": "@vx/legend", + "version": "1.0.0", + "description": "vx legend", + "main": "build/index.js", + "scripts": { + "build": "make build SRC=./src", + "prepublish": "make build SRC=./src", + "test": "jest" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/hshoff/vx.git" + }, + "keywords": [ + "vx", + "react", + "d3", + "visualizations", + "charts" + ], + "author": "@hshoff", + "license": "MIT", + "bugs": { + "url": "https://github.com/hshoff/vx/issues" + }, + "homepage": "https://github.com/hshoff/vx#readme", + "devDependencies": { + "babel-jest": "^20.0.3", + "enzyme": "^2.8.2", + "jest": "^20.0.3", + "react-addons-test-utils": "^15.5.1", + "react-fatigue-dev": "github:tj/react-fatigue-dev", + "react-tools": "^0.10.0", + "regenerator-runtime": "^0.10.5" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@vx/group": "0.0.114", + "classnames": "^2.2.5" + } +} diff --git a/packages/vx-legend/src/index.js b/packages/vx-legend/src/index.js new file mode 100644 index 000000000..a49e94300 --- /dev/null +++ b/packages/vx-legend/src/index.js @@ -0,0 +1 @@ +export { default as Legend } from './legends/Legend'; diff --git a/packages/vx-legend/src/labels/quantile.js b/packages/vx-legend/src/labels/quantile.js new file mode 100644 index 000000000..3aa774648 --- /dev/null +++ b/packages/vx-legend/src/labels/quantile.js @@ -0,0 +1,10 @@ +export default function labelQuantile({ + scale, + labelFormat, + labelDelimiter = '', +}) { + return scale.range().map(d => { + const [x0, x1] = scale.invertExtent(d); + return `${labelFormat(x0)} ${labelDelimiter} ${labelFormat(x1)}`; + }); +} \ No newline at end of file diff --git a/packages/vx-legend/src/legends/Legend.js b/packages/vx-legend/src/legends/Legend.js new file mode 100644 index 000000000..7619d09b0 --- /dev/null +++ b/packages/vx-legend/src/legends/Legend.js @@ -0,0 +1,26 @@ +import React from 'react'; +import Group from '@vx/group'; +import labelQuantile from '../labels/quantile'; + +export default function Legend({ + scale, + labelFormat = d => d, + labelDelimiter = '-', +}) { + const labels = labelQuantile({ + scale, + labelFormat, + labelDelimiter + }); + return ( +
+ {labels.map((label, i) => { + return ( +
+ {label} +
+ ); + })} +
+ ); +} \ No newline at end of file diff --git a/packages/vx-legend/test/Legend.test.js b/packages/vx-legend/test/Legend.test.js new file mode 100644 index 000000000..a9f4c872e --- /dev/null +++ b/packages/vx-legend/test/Legend.test.js @@ -0,0 +1,7 @@ +import { Legend } from '../src'; + +describe('', () => { + test('it should be defined', () => { + expect(Legend).toBeDefined(); + }) +}) \ No newline at end of file diff --git a/packages/vx-scale/src/index.js b/packages/vx-scale/src/index.js index 504e54bea..4fc597202 100644 --- a/packages/vx-scale/src/index.js +++ b/packages/vx-scale/src/index.js @@ -6,6 +6,9 @@ export { default as scaleUtc } from './scales/utc'; export { default as scaleLog } from './scales/log'; export { default as scalePower } from './scales/power'; export { default as scaleOrdinal } from './scales/ordinal'; +export { default as scaleQuantize } from './scales/quantize'; +export { default as scaleQuantile } from './scales/quantile'; +export { default as scaleThreshold } from './scales/threshold'; export { default as schemeCategory10 } from './scales/color/schemeCategory10'; export { default as schemeCategory20 } from './scales/color/schemeCategory20'; export { default as schemeCategory20b } from './scales/color/schemeCategory20b'; diff --git a/packages/vx-scale/src/scales/quantile.js b/packages/vx-scale/src/scales/quantile.js new file mode 100644 index 000000000..8ae37f87f --- /dev/null +++ b/packages/vx-scale/src/scales/quantile.js @@ -0,0 +1,13 @@ +import { scaleQuantile } from 'd3-scale'; + +export default ({ + range, + domain, +}) => { + const scale = scaleQuantile(); + + if (range) scale.range(range); + if (domain) scale.domain(domain); + + return scale; +} diff --git a/packages/vx-scale/src/scales/quantize.js b/packages/vx-scale/src/scales/quantize.js new file mode 100644 index 000000000..2ead8bda9 --- /dev/null +++ b/packages/vx-scale/src/scales/quantize.js @@ -0,0 +1,19 @@ +import { scaleQuantize } from 'd3-scale'; + +export default ({ + range, + domain, + ticks, + tickFormat, + nice = false, +}) => { + const scale = scaleQuantize(); + + if (range) scale.range(range); + if (domain) scale.domain(domain); + if (nice) scale.nice(); + if (ticks) scale.ticks(ticks); + if (tickFormat) scale.tickFormat(tickFormat); + + return scale; +} diff --git a/packages/vx-scale/src/scales/threshold.js b/packages/vx-scale/src/scales/threshold.js new file mode 100644 index 000000000..0b2020ac0 --- /dev/null +++ b/packages/vx-scale/src/scales/threshold.js @@ -0,0 +1,13 @@ +import { scaleThreshold } from 'd3-scale'; + +export default ({ + range, + domain, +}) => { + const scale = scaleThreshold(); + + if (range) scale.range(range); + if (domain) scale.domain(domain); + + return scale; +} diff --git a/packages/vx-scale/test/scaleQuantile.test.js b/packages/vx-scale/test/scaleQuantile.test.js new file mode 100644 index 000000000..ca04f2a95 --- /dev/null +++ b/packages/vx-scale/test/scaleQuantile.test.js @@ -0,0 +1,19 @@ +import { scaleQuantile } from '../src'; + +describe('scaleQuantile', () => { + test('it should be defined', () => { + expect(scaleQuantile).toBeDefined() + }) + + test('range param should set scale range', () => { + const range = [2, 3] + const scale = scaleQuantile({ range }) + expect(scale.range()).toEqual(range) + }) + + test('domain param should set scale domain', () => { + const domain = [0, 350] + const scale = scaleQuantile({ domain }) + expect(scale.domain()).toEqual(domain) + }) +}) \ No newline at end of file diff --git a/packages/vx-scale/test/scaleQuantize.test.js b/packages/vx-scale/test/scaleQuantize.test.js new file mode 100644 index 000000000..f26fcd3d9 --- /dev/null +++ b/packages/vx-scale/test/scaleQuantize.test.js @@ -0,0 +1,19 @@ +import { scaleQuantize } from '../src'; + +describe('scaleQuantize', () => { + test('it should be defined', () => { + expect(scaleQuantize).toBeDefined() + }) + + test('range param should set scale range', () => { + const range = [2, 3] + const scale = scaleQuantize({ range }) + expect(scale.range()).toEqual(range) + }) + + test('domain param should set scale domain', () => { + const domain = [0, 350] + const scale = scaleQuantize({ domain }) + expect(scale.domain()).toEqual(domain) + }) +}) \ No newline at end of file diff --git a/packages/vx-scale/test/scaleThreshold.test.js b/packages/vx-scale/test/scaleThreshold.test.js new file mode 100644 index 000000000..f88a77691 --- /dev/null +++ b/packages/vx-scale/test/scaleThreshold.test.js @@ -0,0 +1,19 @@ +import { scaleThreshold } from '../src'; + +describe('scaleThreshold', () => { + test('it should be defined', () => { + expect(scaleThreshold).toBeDefined() + }) + + test('range param should set scale range', () => { + const range = [2, 3] + const scale = scaleThreshold({ range }) + expect(scale.range()).toEqual(range) + }) + + test('domain param should set scale domain', () => { + const domain = [0, 350] + const scale = scaleThreshold({ domain }) + expect(scale.domain()).toEqual(domain) + }) +}) \ No newline at end of file From 526334f6e328774ebebfc3bf9420760f29240e73 Mon Sep 17 00:00:00 2001 From: Harry Shoff Date: Wed, 14 Jun 2017 21:45:01 -0700 Subject: [PATCH 2/6] [legend] add Linear, Quantile, Ordinal, Threshold --- packages/vx-demo/pages/legends.js | 140 ++++++++++++++++-- packages/vx-legend/package.json | 6 +- packages/vx-legend/src/index.js | 4 + packages/vx-legend/src/labels/linear.js | 21 +++ packages/vx-legend/src/labels/ordinal.js | 11 ++ packages/vx-legend/src/labels/quantile.js | 8 +- packages/vx-legend/src/labels/threshold.js | 31 ++++ packages/vx-legend/src/legends/Legend.js | 83 +++++++++-- packages/vx-legend/src/legends/LegendItem.js | 22 +++ packages/vx-legend/src/legends/LegendLabel.js | 27 ++++ packages/vx-legend/src/legends/LegendShape.js | 24 +++ packages/vx-legend/src/legends/Linear.js | 23 +++ packages/vx-legend/src/legends/Ordinal.js | 21 +++ packages/vx-legend/src/legends/Quantile.js | 23 +++ packages/vx-legend/src/legends/Threshold.js | 27 ++++ .../vx-legend/src/util/renderComponent.js | 0 16 files changed, 444 insertions(+), 27 deletions(-) create mode 100644 packages/vx-legend/src/labels/linear.js create mode 100644 packages/vx-legend/src/labels/ordinal.js create mode 100644 packages/vx-legend/src/labels/threshold.js create mode 100644 packages/vx-legend/src/legends/LegendItem.js create mode 100644 packages/vx-legend/src/legends/LegendLabel.js create mode 100644 packages/vx-legend/src/legends/LegendShape.js create mode 100644 packages/vx-legend/src/legends/Linear.js create mode 100644 packages/vx-legend/src/legends/Ordinal.js create mode 100644 packages/vx-legend/src/legends/Quantile.js create mode 100644 packages/vx-legend/src/legends/Threshold.js create mode 100644 packages/vx-legend/src/util/renderComponent.js diff --git a/packages/vx-demo/pages/legends.js b/packages/vx-demo/pages/legends.js index c980c32c5..f05c88595 100644 --- a/packages/vx-demo/pages/legends.js +++ b/packages/vx-demo/pages/legends.js @@ -1,19 +1,139 @@ -import { Legend } from '@vx/legend'; -import { scaleQuantize } from '@vx/scale'; +import { LegendQuantile, LegendLinear, LegendOrdinal, LegendThreshold } from '@vx/legend'; +import { scaleQuantize, scaleLinear, scaleOrdinal, scaleThreshold } from '@vx/scale'; import { format } from 'd3-format'; -const scale = scaleQuantize({ +const oneDecimalFormat = format('.1f'); +const twoDecimalFormat = format('.2f'); + +const quantile = scaleQuantize({ domain: [0, 0.15], - range: new Array(9).fill(1).map((v,i) => `q${i}-9`) + range: [ + '#feedde', '#fdd0a2', '#fdae6b', + '#fd8d3c', '#f16913', '#d94801', + '#8c2d04' + ] +}); + +const linear = scaleLinear({ + domain: [0, 10], + range: ["#0068af", "#c00029"] +}); + +const ordinal = scaleOrdinal({ + domain: ['a', 'b', 'c', 'd'], + range: ['#160689', '#a72297', '#f68e44', '#f8e126'] +}); + +const threshold = scaleThreshold({ + domain: [0.02, 0.04, 0.06, 0.08, 0.10], + range: ["#f2f0f7", "#dadaeb", "#bcbddc", "#9e9ac8", "#756bb1", "#54278f"] }); export default () => { return ( - - - +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ Linear +
+ [0,2,4].includes(i) ? oneDecimalFormat(d) : ''} + direction="column" + steps={5} + /> +
+
+
+ Ordinal +
+ +
+
+
+ Threshold +
+ !!d ? `${d * 100}%` : ''} + labelAlign='flex-end' + shapeMargin='0 0 2px 4px' + /> +
+ + +
); } \ No newline at end of file diff --git a/packages/vx-legend/package.json b/packages/vx-legend/package.json index 6961d15ed..635043088 100644 --- a/packages/vx-legend/package.json +++ b/packages/vx-legend/package.json @@ -25,7 +25,7 @@ "url": "https://github.com/hshoff/vx/issues" }, "homepage": "https://github.com/hshoff/vx#readme", - "devDependencies": { + "devDependencies": { "babel-jest": "^20.0.3", "enzyme": "^2.8.2", "jest": "^20.0.3", @@ -38,7 +38,7 @@ "access": "public" }, "dependencies": { - "@vx/group": "0.0.114", - "classnames": "^2.2.5" + "classnames": "^2.2.5", + "prop-types": "^15.5.10" } } diff --git a/packages/vx-legend/src/index.js b/packages/vx-legend/src/index.js index a49e94300..14841d800 100644 --- a/packages/vx-legend/src/index.js +++ b/packages/vx-legend/src/index.js @@ -1 +1,5 @@ export { default as Legend } from './legends/Legend'; +export { default as LegendQuantile } from './legends/Quantile'; +export { default as LegendLinear } from './legends/Linear'; +export { default as LegendOrdinal } from './legends/Ordinal'; +export { default as LegendThreshold } from './legends/Threshold'; diff --git a/packages/vx-legend/src/labels/linear.js b/packages/vx-legend/src/labels/linear.js new file mode 100644 index 000000000..5b48883a3 --- /dev/null +++ b/packages/vx-legend/src/labels/linear.js @@ -0,0 +1,21 @@ +export default function labelLinear({ + scale, + steps = 5, + labelFormat, +}) { + const domain = scale.domain(); + const start = domain[0]; + const end = domain[domain.length - 1]; + const step = (end - start) / (steps - 1); + const data = new Array(steps).fill(1).reduce((acc, cur, i) => { + acc.push(start + i * step); + return acc; + }, []); + return data.map((d, i) => { + return { + extent: [], + text: `${labelFormat(d, i)}`, + value: scale(d) + }; + }); +} \ No newline at end of file diff --git a/packages/vx-legend/src/labels/ordinal.js b/packages/vx-legend/src/labels/ordinal.js new file mode 100644 index 000000000..4cbdac5a7 --- /dev/null +++ b/packages/vx-legend/src/labels/ordinal.js @@ -0,0 +1,11 @@ +export default function labelOrdinal({ + scale, + labelFormat, +}) { + return scale.domain().map((d, i) => { + return { + text: `${labelFormat(d, i)}`, + value: scale(d) + }; + }); +} \ No newline at end of file diff --git a/packages/vx-legend/src/labels/quantile.js b/packages/vx-legend/src/labels/quantile.js index 3aa774648..52cdbb0fd 100644 --- a/packages/vx-legend/src/labels/quantile.js +++ b/packages/vx-legend/src/labels/quantile.js @@ -3,8 +3,12 @@ export default function labelQuantile({ labelFormat, labelDelimiter = '', }) { - return scale.range().map(d => { + return scale.range().map((d, i) => { const [x0, x1] = scale.invertExtent(d); - return `${labelFormat(x0)} ${labelDelimiter} ${labelFormat(x1)}`; + return { + extent: [x0, x1], + text: `${labelFormat(x0, i)} ${labelDelimiter} ${labelFormat(x1, i)}`, + value: scale(x0) + }; }); } \ No newline at end of file diff --git a/packages/vx-legend/src/labels/threshold.js b/packages/vx-legend/src/labels/threshold.js new file mode 100644 index 000000000..df6feb0c3 --- /dev/null +++ b/packages/vx-legend/src/labels/threshold.js @@ -0,0 +1,31 @@ +function format( labelFormat, value, i) { + return labelFormat(value, i) || ''; +} + +export default function labelThreshold({ + scale, + labelFormat, + labelDelimiter = 'to', + labelLower = 'Less than ', + labelUpper = 'More than ', +}) { + return scale.range().map((d, i) => { + let [x0, x1] = scale.invertExtent(d); + let delimiter = ` ${labelDelimiter} `; + let value = x1; + if (!x0) { + delimiter = labelLower; + } + if (!x1) { + value = x0; + x1 = x0; + x0 = undefined; + delimiter = labelUpper; + } + return { + extent: [x0, x1], + text: `${format(labelFormat, x0, i)}${delimiter}${format(labelFormat, x1, i)}`, + value: scale(value) + }; + }); +} \ No newline at end of file diff --git a/packages/vx-legend/src/legends/Legend.js b/packages/vx-legend/src/legends/Legend.js index 7619d09b0..8c6fa01c0 100644 --- a/packages/vx-legend/src/legends/Legend.js +++ b/packages/vx-legend/src/legends/Legend.js @@ -1,24 +1,83 @@ import React from 'react'; -import Group from '@vx/group'; +import cx from 'classnames'; +import PropTypes from 'prop-types'; +import LegendItem from './LegendItem'; +import LegendLabel from './LegendLabel'; +import LegendShape from './LegendShape'; import labelQuantile from '../labels/quantile'; +Legend.propTypes = { + className: PropTypes.string, + style: PropTypes.object, + scale: PropTypes.func.isRequired, + labels: PropTypes.arrayOf( + PropTypes.shape({ + extend: PropTypes.array, + text: PropTypes.string, + value: PropTypes.any, + }) + ), + shapeWidth: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]), + shapeHeight: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]), + shapeMargin: PropTypes.string, + labelMargin: PropTypes.string, + itemMargin: PropTypes.string, + direction: PropTypes.string, + itemDirection: PropTypes.string, +}; + export default function Legend({ + className, + style, scale, - labelFormat = d => d, - labelDelimiter = '-', + shape, + labels, + shapeWidth = 15, + shapeHeight = 15, + shapeMargin = '2px 4px 2px 0', + labelAlign = 'left', + labelMargin = '0 4px', + itemMargin = '0', + direction = 'column', + itemDirection = 'row', + ...restProps, }) { - const labels = labelQuantile({ - scale, - labelFormat, - labelDelimiter - }); return ( -
+
{labels.map((label, i) => { + const { text, value } = label; return ( -
- {label} -
+ + + + ); })}
diff --git a/packages/vx-legend/src/legends/LegendItem.js b/packages/vx-legend/src/legends/LegendItem.js new file mode 100644 index 000000000..7a1ae307f --- /dev/null +++ b/packages/vx-legend/src/legends/LegendItem.js @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export default function LegendItem({ + children, + flexDirection, + margin, +}) { + return ( +
+ {children} +
+ ); +} \ No newline at end of file diff --git a/packages/vx-legend/src/legends/LegendLabel.js b/packages/vx-legend/src/legends/LegendLabel.js new file mode 100644 index 000000000..16d659da8 --- /dev/null +++ b/packages/vx-legend/src/legends/LegendLabel.js @@ -0,0 +1,27 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +LegendLabel.propTypes = { + label: PropTypes.string.isRequired, + margin: PropTypes.string.isRequired, +}; + +export default function LegendLabel({ + label, + margin, + align, +}) { + return ( +
+ {label} +
+ ); +} \ No newline at end of file diff --git a/packages/vx-legend/src/legends/LegendShape.js b/packages/vx-legend/src/legends/LegendShape.js new file mode 100644 index 000000000..b7e386257 --- /dev/null +++ b/packages/vx-legend/src/legends/LegendShape.js @@ -0,0 +1,24 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export default function LegendShape({ + shape, + width, + height, + margin, + value, +}) { + return ( +
+ {shape} +
+ ); +} \ No newline at end of file diff --git a/packages/vx-legend/src/legends/Linear.js b/packages/vx-legend/src/legends/Linear.js new file mode 100644 index 000000000..c36a748d6 --- /dev/null +++ b/packages/vx-legend/src/legends/Linear.js @@ -0,0 +1,23 @@ +import React from 'react'; +import Legend from './Legend'; +import labelLinear from '../labels/linear'; + +export default function LegendLinear({ + scale, + labelFormat = x => x, + steps, + ...restProps, +}) { + const labels = labelLinear({ + scale, + steps, + labelFormat, + }); + return ( + + ); +} \ No newline at end of file diff --git a/packages/vx-legend/src/legends/Ordinal.js b/packages/vx-legend/src/legends/Ordinal.js new file mode 100644 index 000000000..12ae36343 --- /dev/null +++ b/packages/vx-legend/src/legends/Ordinal.js @@ -0,0 +1,21 @@ +import React from 'react'; +import Legend from './Legend'; +import labelOrdinal from '../labels/ordinal'; + +export default function LegendOrdinal({ + scale, + labelFormat = x => x, + ...restProps, +}) { + const labels = labelOrdinal({ + scale, + labelFormat, + }); + return ( + + ); +} \ No newline at end of file diff --git a/packages/vx-legend/src/legends/Quantile.js b/packages/vx-legend/src/legends/Quantile.js new file mode 100644 index 000000000..8964eef8a --- /dev/null +++ b/packages/vx-legend/src/legends/Quantile.js @@ -0,0 +1,23 @@ +import React from 'react'; +import Legend from './Legend'; +import labelQuantile from '../labels/quantile'; + +export default function LegendQuantile({ + scale, + labelFormat = x => x, + labelDelimiter = '-', + ...restProps, +}) { + const labels = labelQuantile({ + scale, + labelFormat, + labelDelimiter + }); + return ( + + ); +} \ No newline at end of file diff --git a/packages/vx-legend/src/legends/Threshold.js b/packages/vx-legend/src/legends/Threshold.js new file mode 100644 index 000000000..d2bd9c018 --- /dev/null +++ b/packages/vx-legend/src/legends/Threshold.js @@ -0,0 +1,27 @@ +import React from 'react'; +import Legend from './Legend'; +import labelThreshold from '../labels/threshold'; + +export default function LegendThreshold({ + scale, + labelFormat = x => x, + labelDelimiter = 'to', + labelLower = 'Less than ', + labelUpper = 'More than ', + ...restProps, +}) { + const labels = labelThreshold({ + scale, + labelFormat, + labelDelimiter, + labelLower, + labelUpper, + }); + return ( + + ); +} \ No newline at end of file diff --git a/packages/vx-legend/src/util/renderComponent.js b/packages/vx-legend/src/util/renderComponent.js new file mode 100644 index 000000000..e69de29bb From 68ca6025167ce3d5cfaaa64d2a34dd64086ecb10 Mon Sep 17 00:00:00 2001 From: Harry Shoff Date: Wed, 14 Jun 2017 21:52:09 -0700 Subject: [PATCH 3/6] [legends] add initial tests --- packages/vx-demo/pages/legends.js | 14 ++++++++++++-- packages/vx-legend/test/LegendLinear.test.js | 7 +++++++ packages/vx-legend/test/LegendOrdinal.test.js | 7 +++++++ packages/vx-legend/test/LegendQuantile.test.js | 7 +++++++ packages/vx-legend/test/LegendThreshold.test.js | 7 +++++++ 5 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 packages/vx-legend/test/LegendLinear.test.js create mode 100644 packages/vx-legend/test/LegendOrdinal.test.js create mode 100644 packages/vx-legend/test/LegendQuantile.test.js create mode 100644 packages/vx-legend/test/LegendThreshold.test.js diff --git a/packages/vx-demo/pages/legends.js b/packages/vx-demo/pages/legends.js index f05c88595..1b07f0ff7 100644 --- a/packages/vx-demo/pages/legends.js +++ b/packages/vx-demo/pages/legends.js @@ -1,5 +1,15 @@ -import { LegendQuantile, LegendLinear, LegendOrdinal, LegendThreshold } from '@vx/legend'; -import { scaleQuantize, scaleLinear, scaleOrdinal, scaleThreshold } from '@vx/scale'; +import { + LegendQuantile, + LegendLinear, + LegendOrdinal, + LegendThreshold, +} from '@vx/legend'; +import { + scaleQuantize, + scaleLinear, + scaleOrdinal, + scaleThreshold, +} from '@vx/scale'; import { format } from 'd3-format'; const oneDecimalFormat = format('.1f'); diff --git a/packages/vx-legend/test/LegendLinear.test.js b/packages/vx-legend/test/LegendLinear.test.js new file mode 100644 index 000000000..b838516dc --- /dev/null +++ b/packages/vx-legend/test/LegendLinear.test.js @@ -0,0 +1,7 @@ +import { LegendLinear } from '../src'; + +describe('', () => { + test('it should be defined', () => { + expect(LegendLinear).toBeDefined(); + }) +}) \ No newline at end of file diff --git a/packages/vx-legend/test/LegendOrdinal.test.js b/packages/vx-legend/test/LegendOrdinal.test.js new file mode 100644 index 000000000..c658e639b --- /dev/null +++ b/packages/vx-legend/test/LegendOrdinal.test.js @@ -0,0 +1,7 @@ +import { LegendOrdinal } from '../src'; + +describe('', () => { + test('it should be defined', () => { + expect(LegendOrdinal).toBeDefined(); + }) +}) \ No newline at end of file diff --git a/packages/vx-legend/test/LegendQuantile.test.js b/packages/vx-legend/test/LegendQuantile.test.js new file mode 100644 index 000000000..60234c510 --- /dev/null +++ b/packages/vx-legend/test/LegendQuantile.test.js @@ -0,0 +1,7 @@ +import { LegendQuantile } from '../src'; + +describe('', () => { + test('it should be defined', () => { + expect(LegendQuantile).toBeDefined(); + }) +}) \ No newline at end of file diff --git a/packages/vx-legend/test/LegendThreshold.test.js b/packages/vx-legend/test/LegendThreshold.test.js new file mode 100644 index 000000000..994f71274 --- /dev/null +++ b/packages/vx-legend/test/LegendThreshold.test.js @@ -0,0 +1,7 @@ +import { LegendThreshold } from '../src'; + +describe('', () => { + test('it should be defined', () => { + expect(LegendThreshold).toBeDefined(); + }) +}) \ No newline at end of file From 61a97f7cc0636a2e2101fb6e4e90f90ec58c775e Mon Sep 17 00:00:00 2001 From: Harry Shoff Date: Tue, 27 Jun 2017 21:25:18 -0700 Subject: [PATCH 4/6] [legend] new legends --- packages/vx-demo/pages/legends.js | 263 +++++++++++------- packages/vx-glyph/package.json | 5 +- packages/vx-glyph/src/glyphs/Cross.js | 31 +++ packages/vx-glyph/src/index.js | 1 + packages/vx-glyph/src/util/additionalProps.js | 8 + packages/vx-glyph/src/util/callOrValue.js | 6 + packages/vx-legend/Readme.md | 67 +++++ packages/vx-legend/package.json | 1 + packages/vx-legend/src/index.js | 1 + packages/vx-legend/src/labels/linear.js | 21 -- packages/vx-legend/src/labels/ordinal.js | 11 - packages/vx-legend/src/labels/quantile.js | 14 - packages/vx-legend/src/labels/threshold.js | 31 --- packages/vx-legend/src/legends/Legend.js | 47 +++- packages/vx-legend/src/legends/LegendShape.js | 28 +- packages/vx-legend/src/legends/Linear.js | 48 +++- packages/vx-legend/src/legends/Ordinal.js | 37 ++- packages/vx-legend/src/legends/Quantile.js | 46 ++- packages/vx-legend/src/legends/Size.js | 45 +++ packages/vx-legend/src/legends/Threshold.js | 74 ++++- packages/vx-legend/src/shapes/Circle.js | 16 ++ packages/vx-legend/src/shapes/Rect.js | 14 + .../vx-legend/src/util/renderComponent.js | 0 packages/vx-legend/src/util/renderShape.js | 33 +++ .../vx-legend/src/util/valueOrIdentity.js | 4 + packages/vx-legend/test/LegendSize.test.js | 7 + 26 files changed, 623 insertions(+), 236 deletions(-) create mode 100644 packages/vx-glyph/src/glyphs/Cross.js create mode 100644 packages/vx-glyph/src/util/additionalProps.js create mode 100644 packages/vx-glyph/src/util/callOrValue.js create mode 100644 packages/vx-legend/Readme.md delete mode 100644 packages/vx-legend/src/labels/linear.js delete mode 100644 packages/vx-legend/src/labels/ordinal.js delete mode 100644 packages/vx-legend/src/labels/quantile.js delete mode 100644 packages/vx-legend/src/labels/threshold.js create mode 100644 packages/vx-legend/src/legends/Size.js create mode 100644 packages/vx-legend/src/shapes/Circle.js create mode 100644 packages/vx-legend/src/shapes/Rect.js delete mode 100644 packages/vx-legend/src/util/renderComponent.js create mode 100644 packages/vx-legend/src/util/renderShape.js create mode 100644 packages/vx-legend/src/util/valueOrIdentity.js create mode 100644 packages/vx-legend/test/LegendSize.test.js diff --git a/packages/vx-demo/pages/legends.js b/packages/vx-demo/pages/legends.js index 1b07f0ff7..3fe9ba368 100644 --- a/packages/vx-demo/pages/legends.js +++ b/packages/vx-demo/pages/legends.js @@ -1,8 +1,13 @@ +import { format } from 'd3-format'; +import { Group } from '@vx/group'; +import { GlyphCross } from '@vx/glyph'; import { LegendQuantile, LegendLinear, LegendOrdinal, LegendThreshold, + LegendSize, + Legend, } from '@vx/legend'; import { scaleQuantize, @@ -10,122 +15,91 @@ import { scaleOrdinal, scaleThreshold, } from '@vx/scale'; -import { format } from 'd3-format'; const oneDecimalFormat = format('.1f'); const twoDecimalFormat = format('.2f'); const quantile = scaleQuantize({ domain: [0, 0.15], - range: [ - '#feedde', '#fdd0a2', '#fdae6b', - '#fd8d3c', '#f16913', '#d94801', - '#8c2d04' - ] + range: ['#fdd0a2', '#fdae6b', '#fd8d3c', '#f16913', '#d94801'], }); const linear = scaleLinear({ domain: [0, 10], - range: ["#0068af", "#c00029"] + range: ['#0068af', '#c00029'], }); -const ordinal = scaleOrdinal({ +const ordinalColor = scaleOrdinal({ domain: ['a', 'b', 'c', 'd'], - range: ['#160689', '#a72297', '#f68e44', '#f8e126'] + range: ['#160689', '#a72297', '#f68e44', '#f8e126'].reverse(), +}); + +const ordinalShape = scaleOrdinal({ + domain: ['a', 'b', 'c', 'd'], + range: [ + props => + , + props => , + props => + , + props => + + $ + , + ], }); const threshold = scaleThreshold({ - domain: [0.02, 0.04, 0.06, 0.08, 0.10], - range: ["#f2f0f7", "#dadaeb", "#bcbddc", "#9e9ac8", "#756bb1", "#54278f"] + domain: [0.02, 0.04, 0.06, 0.08, 0.1], + range: [ + '#f2f0f7', + '#dadaeb', + '#bcbddc', + '#9e9ac8', + '#756bb1', + '#54278f', + ], }); -export default () => { +const size = scaleLinear({ + domain: [0, 10], + range: [10, 30], +}); + +const sizeOpacity = scaleLinear({ + domain: [0, 10], + range: [0.4, 1], +}); + +const SizeItem = ({ width, height, scale, fill, label }) => { + const radius = Math.max(width, height) / 2; return ( -
-
- -
-
- -
-
- -
-
- -
-
-
- Linear -
- [0,2,4].includes(i) ? oneDecimalFormat(d) : ''} - direction="column" - steps={5} - /> -
-
-
- Ordinal -
- -
-
-
- Threshold -
- !!d ? `${d * 100}%` : ''} - labelAlign='flex-end' - shapeMargin='0 0 2px 4px' - /> -
+ + + + ); +}; +function LegendDemo({ title, children }) { + return ( +
+
{title}
+ {children}
); -} \ No newline at end of file +} + +export default () => { + return ( +
+
+ + { + return { + fill: '#b2212b', + fillOpacity: sizeOpacity(props.datum), + }; + }} + shape={props => { + const { size } = props; + return ( + + + + ); + }} + /> + + + + + + { + if (i % 2 === 0) return oneDecimalFormat(d); + return ''; + }} + /> + + + + + + ordinalColor(datum)} + labelFormat={label => `Type ${label.toUpperCase()}`} + /> + + + ordinalColor(datum)} + shape={props => { + return ( + + {React.createElement( + ordinalShape(props.label.datum), + { + ...props, + }, + )} + + ); + }} + /> + +
+ + +
+ ); +}; diff --git a/packages/vx-glyph/package.json b/packages/vx-glyph/package.json index b0be0ca2a..730927150 100644 --- a/packages/vx-glyph/package.json +++ b/packages/vx-glyph/package.json @@ -46,6 +46,7 @@ }, "dependencies": { "@vx/group": "0.0.120", - "classnames": "^2.2.5" + "classnames": "^2.2.5", + "d3-shape": "^1.2.0" } -} +} \ No newline at end of file diff --git a/packages/vx-glyph/src/glyphs/Cross.js b/packages/vx-glyph/src/glyphs/Cross.js new file mode 100644 index 000000000..0cc931c01 --- /dev/null +++ b/packages/vx-glyph/src/glyphs/Cross.js @@ -0,0 +1,31 @@ +import React from 'react'; +import cx from 'classnames'; +import { symbol, symbolCross } from 'd3-shape'; +import { Glyph } from './Glyph'; +import additionalProps from '../util/additionalProps'; + +export default function GlyphCross({ + children, + className, + top, + left, + size, + ...restProps +}) { + const path = symbol(); + path.type(symbolCross); + if (size) path.size(size); + return ( + + + {children} + + ); +} \ No newline at end of file diff --git a/packages/vx-glyph/src/index.js b/packages/vx-glyph/src/index.js index 40b34429e..1dd007afc 100644 --- a/packages/vx-glyph/src/index.js +++ b/packages/vx-glyph/src/index.js @@ -1,2 +1,3 @@ export { default as Glyph } from './glyphs/Glyph'; export { default as GlyphDot } from './glyphs/Dot'; +export { default as GlyphCross } from './glyphs/Cross'; diff --git a/packages/vx-glyph/src/util/additionalProps.js b/packages/vx-glyph/src/util/additionalProps.js new file mode 100644 index 000000000..b3ac93bc3 --- /dev/null +++ b/packages/vx-glyph/src/util/additionalProps.js @@ -0,0 +1,8 @@ +import callOrValue from './callOrValue'; + +export default function additionalProps(restProps, data) { + return Object.keys(restProps).reduce((ret, cur) => { + ret[cur] = callOrValue(restProps[cur], data); + return ret; + }, {}); +} diff --git a/packages/vx-glyph/src/util/callOrValue.js b/packages/vx-glyph/src/util/callOrValue.js new file mode 100644 index 000000000..77afb1a09 --- /dev/null +++ b/packages/vx-glyph/src/util/callOrValue.js @@ -0,0 +1,6 @@ +export default function callOrValue(maybeFn, data) { + if (typeof maybeFn === 'function') { + return maybeFn(data); + } + return maybeFn; +} diff --git a/packages/vx-legend/Readme.md b/packages/vx-legend/Readme.md new file mode 100644 index 000000000..8c256e16b --- /dev/null +++ b/packages/vx-legend/Readme.md @@ -0,0 +1,67 @@ +# @vx/legend + +Legends associate shapes and colors to data. + +```js +// legends for linear scales +import { LegendLinear } from '@vx/legend'; + +// legends for quantile scales +import { LegendQuantile } from '@vx/legend'; + +// legends for ordinal scales +import { LegendOrdinal } from '@vx/legend'; + +// legends for size scales +import { LegendSize } from '@vx/legend'; + +// legends for threshold scales +import { LegendThreshold } from '@vx/legend'; + +// custom legends +import { Legend } from '@vx/legend'; +``` + +## API + +#### LegendLinear +#### LegendQuantile +#### LegendOrdinal +#### LegendThreshold +#### LegendSize +#### Legend + + +## Example + +```js +import { LegendThreshold } from '@vx/legend'; +import { scaleThreshold } from '@vx/scale'; + +const threshold = scaleThreshold({ + domain: [0.02, 0.04, 0.06, 0.08, 0.1], + range: [ + '#f2f0f7', + '#dadaeb', + '#bcbddc', + '#9e9ac8', + '#756bb1', + '#54278f', + ], +}); + +function MyChart() { + return ( +
+ {/** chart stuff */} + +
+ ); +} +``` \ No newline at end of file diff --git a/packages/vx-legend/package.json b/packages/vx-legend/package.json index 635043088..c7b684a15 100644 --- a/packages/vx-legend/package.json +++ b/packages/vx-legend/package.json @@ -38,6 +38,7 @@ "access": "public" }, "dependencies": { + "@vx/group": "0.0.114", "classnames": "^2.2.5", "prop-types": "^15.5.10" } diff --git a/packages/vx-legend/src/index.js b/packages/vx-legend/src/index.js index 14841d800..099babbaf 100644 --- a/packages/vx-legend/src/index.js +++ b/packages/vx-legend/src/index.js @@ -3,3 +3,4 @@ export { default as LegendQuantile } from './legends/Quantile'; export { default as LegendLinear } from './legends/Linear'; export { default as LegendOrdinal } from './legends/Ordinal'; export { default as LegendThreshold } from './legends/Threshold'; +export { default as LegendSize } from './legends/Size'; diff --git a/packages/vx-legend/src/labels/linear.js b/packages/vx-legend/src/labels/linear.js deleted file mode 100644 index 5b48883a3..000000000 --- a/packages/vx-legend/src/labels/linear.js +++ /dev/null @@ -1,21 +0,0 @@ -export default function labelLinear({ - scale, - steps = 5, - labelFormat, -}) { - const domain = scale.domain(); - const start = domain[0]; - const end = domain[domain.length - 1]; - const step = (end - start) / (steps - 1); - const data = new Array(steps).fill(1).reduce((acc, cur, i) => { - acc.push(start + i * step); - return acc; - }, []); - return data.map((d, i) => { - return { - extent: [], - text: `${labelFormat(d, i)}`, - value: scale(d) - }; - }); -} \ No newline at end of file diff --git a/packages/vx-legend/src/labels/ordinal.js b/packages/vx-legend/src/labels/ordinal.js deleted file mode 100644 index 4cbdac5a7..000000000 --- a/packages/vx-legend/src/labels/ordinal.js +++ /dev/null @@ -1,11 +0,0 @@ -export default function labelOrdinal({ - scale, - labelFormat, -}) { - return scale.domain().map((d, i) => { - return { - text: `${labelFormat(d, i)}`, - value: scale(d) - }; - }); -} \ No newline at end of file diff --git a/packages/vx-legend/src/labels/quantile.js b/packages/vx-legend/src/labels/quantile.js deleted file mode 100644 index 52cdbb0fd..000000000 --- a/packages/vx-legend/src/labels/quantile.js +++ /dev/null @@ -1,14 +0,0 @@ -export default function labelQuantile({ - scale, - labelFormat, - labelDelimiter = '', -}) { - return scale.range().map((d, i) => { - const [x0, x1] = scale.invertExtent(d); - return { - extent: [x0, x1], - text: `${labelFormat(x0, i)} ${labelDelimiter} ${labelFormat(x1, i)}`, - value: scale(x0) - }; - }); -} \ No newline at end of file diff --git a/packages/vx-legend/src/labels/threshold.js b/packages/vx-legend/src/labels/threshold.js deleted file mode 100644 index df6feb0c3..000000000 --- a/packages/vx-legend/src/labels/threshold.js +++ /dev/null @@ -1,31 +0,0 @@ -function format( labelFormat, value, i) { - return labelFormat(value, i) || ''; -} - -export default function labelThreshold({ - scale, - labelFormat, - labelDelimiter = 'to', - labelLower = 'Less than ', - labelUpper = 'More than ', -}) { - return scale.range().map((d, i) => { - let [x0, x1] = scale.invertExtent(d); - let delimiter = ` ${labelDelimiter} `; - let value = x1; - if (!x0) { - delimiter = labelLower; - } - if (!x1) { - value = x0; - x1 = x0; - x0 = undefined; - delimiter = labelUpper; - } - return { - extent: [x0, x1], - text: `${format(labelFormat, x0, i)}${delimiter}${format(labelFormat, x1, i)}`, - value: scale(value) - }; - }); -} \ No newline at end of file diff --git a/packages/vx-legend/src/legends/Legend.js b/packages/vx-legend/src/legends/Legend.js index 8c6fa01c0..0c08f364c 100644 --- a/packages/vx-legend/src/legends/Legend.js +++ b/packages/vx-legend/src/legends/Legend.js @@ -4,19 +4,13 @@ import PropTypes from 'prop-types'; import LegendItem from './LegendItem'; import LegendLabel from './LegendLabel'; import LegendShape from './LegendShape'; -import labelQuantile from '../labels/quantile'; +import valueOrIdentity from '../util/valueOrIdentity'; Legend.propTypes = { className: PropTypes.string, style: PropTypes.object, - scale: PropTypes.func.isRequired, - labels: PropTypes.arrayOf( - PropTypes.shape({ - extend: PropTypes.array, - text: PropTypes.string, - value: PropTypes.any, - }) - ), + scale: PropTypes.oneOfType([PropTypes.func, PropTypes.object]) + .isRequired, shapeWidth: PropTypes.oneOfType([ PropTypes.number, PropTypes.string, @@ -30,14 +24,23 @@ Legend.propTypes = { itemMargin: PropTypes.string, direction: PropTypes.string, itemDirection: PropTypes.string, + fill: PropTypes.func, + shape: PropTypes.func, + labelFormat: PropTypes.func, + labelTransform: PropTypes.func, }; export default function Legend({ className, style, + shapeStyle, scale, shape, - labels, + domain, + fill = valueOrIdentity, + size = valueOrIdentity, + labelFormat = valueOrIdentity, + labelTransform = defaultTransform, shapeWidth = 15, shapeHeight = 15, shapeMargin = '2px 4px 2px 0', @@ -46,8 +49,10 @@ export default function Legend({ itemMargin = '0', direction = 'column', itemDirection = 'row', - ...restProps, + ...restProps }) { + domain = domain || scale.domain(); + const labels = domain.map(labelTransform({ scale, labelFormat })); return (
{labels.map((label, i) => { - const { text, value } = label; + const { text } = label; return ( ); -} \ No newline at end of file +} + +function defaultTransform({ scale, labelFormat }) { + return (d, i) => { + return { + datum: d, + index: i, + text: `${labelFormat(d, i)}`, + value: scale(d), + }; + }; +} diff --git a/packages/vx-legend/src/legends/LegendShape.js b/packages/vx-legend/src/legends/LegendShape.js index b7e386257..6fd15b864 100644 --- a/packages/vx-legend/src/legends/LegendShape.js +++ b/packages/vx-legend/src/legends/LegendShape.js @@ -1,24 +1,36 @@ import React from 'react'; import PropTypes from 'prop-types'; +import ShapeRect from '../shapes/Rect'; +import renderShape from '../util/renderShape'; export default function LegendShape({ - shape, + shape = ShapeRect, width, height, margin, - value, + label, + fill, + size, + shapeStyle, }) { return (
- {shape} + {renderShape({ + shape, + label, + width, + height, + fill, + shapeStyle, + })}
); -} \ No newline at end of file +} diff --git a/packages/vx-legend/src/legends/Linear.js b/packages/vx-legend/src/legends/Linear.js index c36a748d6..b286e9f0e 100644 --- a/packages/vx-legend/src/legends/Linear.js +++ b/packages/vx-legend/src/legends/Linear.js @@ -1,23 +1,51 @@ import React from 'react'; +import PropTypes from 'prop-types'; import Legend from './Legend'; -import labelLinear from '../labels/linear'; + +LegendLinear.propTypes = { + scale: PropTypes.func.isRequired, + domain: PropTypes.array, + steps: PropTypes.number, + labelFormat: PropTypes.func, + labelTransform: PropTypes.func, +}; export default function LegendLinear({ scale, + domain, + steps = 5, labelFormat = x => x, - steps, - ...restProps, + labelTransform = defaultTransform, + ...restProps }) { - const labels = labelLinear({ - scale, - steps, - labelFormat, - }); + domain = domain || defaultDomain({ steps, scale }); return ( ); -} \ No newline at end of file +} + +function defaultDomain({ steps, scale }) { + const domain = scale.domain(); + const start = domain[0]; + const end = domain[domain.length - 1]; + const step = (end - start) / (steps - 1); + return new Array(steps).fill(1).reduce((acc, cur, i) => { + acc.push(start + i * step); + return acc; + }, []); +} + +function defaultTransform({ scale, labelFormat }) { + return (d, i) => { + return { + text: `${labelFormat(d, i)}`, + value: scale(d), + }; + }; +} diff --git a/packages/vx-legend/src/legends/Ordinal.js b/packages/vx-legend/src/legends/Ordinal.js index 12ae36343..e7e894f1a 100644 --- a/packages/vx-legend/src/legends/Ordinal.js +++ b/packages/vx-legend/src/legends/Ordinal.js @@ -1,21 +1,40 @@ import React from 'react'; +import PropTypes from 'prop-types'; import Legend from './Legend'; -import labelOrdinal from '../labels/ordinal'; +import valueOrIdentity from '../util/valueOrIdentity'; + +LegendOrdinal.propTypes = { + scale: PropTypes.func.isRequired, + domain: PropTypes.array, + labelTransform: PropTypes.func, + labelFormat: PropTypes.func, +}; export default function LegendOrdinal({ scale, - labelFormat = x => x, - ...restProps, + domain, + labelTransform = defaultTransform, + labelFormat = valueOrIdentity, + ...restProps }) { - const labels = labelOrdinal({ - scale, - labelFormat, - }); return ( ); -} \ No newline at end of file +} + +function defaultTransform({ scale, labelFormat }) { + return (d, i) => { + return { + datum: d, + index: i, + text: `${labelFormat(d, i)}`, + value: scale(d), + }; + }; +} diff --git a/packages/vx-legend/src/legends/Quantile.js b/packages/vx-legend/src/legends/Quantile.js index 8964eef8a..f8cd1673f 100644 --- a/packages/vx-legend/src/legends/Quantile.js +++ b/packages/vx-legend/src/legends/Quantile.js @@ -1,23 +1,51 @@ import React from 'react'; +import PropTypes from 'prop-types'; import Legend from './Legend'; -import labelQuantile from '../labels/quantile'; + +LegendQuantile.propTypes = { + scale: PropTypes.func.isRequired, + domain: PropTypes.array, + labelFormat: PropTypes.func, + labelTransform: PropTypes.func, + labelDelimiter: PropTypes.string, +}; export default function LegendQuantile({ scale, + domain, labelFormat = x => x, + labelTransform, labelDelimiter = '-', - ...restProps, + ...restProps }) { - const labels = labelQuantile({ - scale, - labelFormat, - labelDelimiter - }); + domain = domain || scale.range(); + labelTransform = + labelTransform || defaultTransform({ labelDelimiter }); return ( ); -} \ No newline at end of file +} + +function defaultTransform({ labelDelimiter }) { + return ({ scale, labelFormat }) => { + return (d, i) => { + const [x0, x1] = scale.invertExtent(d); + return { + extent: [x0, x1], + text: `${labelFormat(x0, i)} ${labelDelimiter} ${labelFormat( + x1, + i, + )}`, + value: scale(x0), + datum: d, + index: i, + }; + }; + }; +} diff --git a/packages/vx-legend/src/legends/Size.js b/packages/vx-legend/src/legends/Size.js new file mode 100644 index 000000000..0b51f0fc6 --- /dev/null +++ b/packages/vx-legend/src/legends/Size.js @@ -0,0 +1,45 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Legend from './Legend'; + +export default function LegendSize({ + scale, + domain, + steps = 5, + labelFormat = x => x, + labelTransform = defaultTransform, + ...restProps +}) { + domain = domain || defaultDomain({ steps, scale }); + return ( + + ); +} + +function defaultDomain({ steps, scale }) { + const domain = scale.domain(); + const start = domain[0]; + const end = domain[domain.length - 1]; + const step = (end - start) / (steps - 1); + return new Array(steps).fill(1).reduce((acc, cur, i) => { + acc.push(start + i * step); + return acc; + }, []); +} + +function defaultTransform({ scale, labelFormat }) { + return (d, i) => { + return { + text: `${labelFormat(d, i)}`, + value: scale(d), + datum: d, + index: i, + }; + }; +} diff --git a/packages/vx-legend/src/legends/Threshold.js b/packages/vx-legend/src/legends/Threshold.js index d2bd9c018..c14bf787d 100644 --- a/packages/vx-legend/src/legends/Threshold.js +++ b/packages/vx-legend/src/legends/Threshold.js @@ -1,27 +1,79 @@ import React from 'react'; +import PropTypes from 'prop-types'; import Legend from './Legend'; -import labelThreshold from '../labels/threshold'; + +LegendThreshold.propTypes = { + scale: PropTypes.func.isRequired, + domain: PropTypes.array, + labelTransform: PropTypes.func, + labelFormat: PropTypes.func, + labelDelimiter: PropTypes.string, + labelLower: PropTypes.string, + labelUpper: PropTypes.string, +}; export default function LegendThreshold({ scale, + domain, labelFormat = x => x, + labelTransform, labelDelimiter = 'to', labelLower = 'Less than ', labelUpper = 'More than ', - ...restProps, + ...restProps }) { - const labels = labelThreshold({ - scale, - labelFormat, - labelDelimiter, - labelLower, - labelUpper, - }); + domain = domain || scale.range(); + labelTransform = + labelTransform || + defaultTransform({ + labelDelimiter, + labelLower, + labelUpper, + }); return ( ); -} \ No newline at end of file +} + +function defaultTransform({ + labelDelimiter, + labelLower, + labelUpper, +}) { + return ({ scale, labelFormat }) => { + function format(labelFormat, value, i) { + return labelFormat(value, i) || ''; + } + return (d, i) => { + let [x0, x1] = scale.invertExtent(d); + let delimiter = ` ${labelDelimiter} `; + let value = x1; + if (!x0) { + delimiter = labelLower; + } + if (!x1) { + value = x0; + x1 = x0; + x0 = undefined; + delimiter = labelUpper; + } + return { + extent: [x0, x1], + text: `${format(labelFormat, x0, i)}${delimiter}${format( + labelFormat, + x1, + i, + )}`, + value: scale(value), + datum: d, + index: i, + }; + }; + }; +} diff --git a/packages/vx-legend/src/shapes/Circle.js b/packages/vx-legend/src/shapes/Circle.js new file mode 100644 index 000000000..44d81b774 --- /dev/null +++ b/packages/vx-legend/src/shapes/Circle.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { Group } from '@vx/group'; + +export default function ShapeCircle({ fill, width, height, style }) { + if (typeof width === 'string') width = 0; + if (typeof height === 'string') height = 0; + const size = Math.max(width, height); + const radius = size / 2; + return ( + + + + + + ); +} diff --git a/packages/vx-legend/src/shapes/Rect.js b/packages/vx-legend/src/shapes/Rect.js new file mode 100644 index 000000000..4aef21d36 --- /dev/null +++ b/packages/vx-legend/src/shapes/Rect.js @@ -0,0 +1,14 @@ +import React from 'react'; + +export default function ShapeRect({ fill, width, height, style }) { + return ( +
+ ); +} diff --git a/packages/vx-legend/src/util/renderComponent.js b/packages/vx-legend/src/util/renderComponent.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/vx-legend/src/util/renderShape.js b/packages/vx-legend/src/util/renderShape.js new file mode 100644 index 000000000..624a2dddc --- /dev/null +++ b/packages/vx-legend/src/util/renderShape.js @@ -0,0 +1,33 @@ +import React from 'react'; +import ShapeRect from '../shapes/Rect'; +import ShapeCircle from '../shapes/Circle'; +import valueOrIdentity from './valueOrIdentity'; + +export default function renderShape({ + shape = 'rect', + fill = valueOrIdentity, + size = valueOrIdentity, + width, + height, + label, + shapeStyle = x => undefined, +}) { + const props = { + width, + height, + label, + fill: fill({ ...label }), + size: size({ ...label }), + style: shapeStyle({ ...label }), + }; + if (typeof shape === 'string') { + if (shape === 'rect') { + return ; + } + return ; + } + if (React.isValidElement(shape)) { + return React.cloneElement(shape, props); + } + return React.createElement(shape, props); +} diff --git a/packages/vx-legend/src/util/valueOrIdentity.js b/packages/vx-legend/src/util/valueOrIdentity.js new file mode 100644 index 000000000..32692db06 --- /dev/null +++ b/packages/vx-legend/src/util/valueOrIdentity.js @@ -0,0 +1,4 @@ +export default function valueOrIdentity(x) { + if (x && x.value) return x.value; + return x; +} diff --git a/packages/vx-legend/test/LegendSize.test.js b/packages/vx-legend/test/LegendSize.test.js new file mode 100644 index 000000000..fc9f1f097 --- /dev/null +++ b/packages/vx-legend/test/LegendSize.test.js @@ -0,0 +1,7 @@ +import { LegendSize } from '../src'; + +describe('', () => { + test('it should be defined', () => { + expect(LegendSize).toBeDefined(); + }) +}) \ No newline at end of file From 8a9efb85f5dcd9ff946b14b6fbc6f48455f391eb Mon Sep 17 00:00:00 2001 From: Harry Shoff Date: Tue, 27 Jun 2017 21:32:53 -0700 Subject: [PATCH 5/6] [legend] add react to peer + devDeps, but group --- packages/vx-legend/package.json | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/vx-legend/package.json b/packages/vx-legend/package.json index c7b684a15..31eaf5835 100644 --- a/packages/vx-legend/package.json +++ b/packages/vx-legend/package.json @@ -32,14 +32,18 @@ "react-addons-test-utils": "^15.5.1", "react-fatigue-dev": "github:tj/react-fatigue-dev", "react-tools": "^0.10.0", - "regenerator-runtime": "^0.10.5" + "regenerator-runtime": "^0.10.5", + "react": "^15.0.0 || 15.x" + }, + "peerDependencies": { + "react": "^15.0.0 || 15.x" }, "publishConfig": { "access": "public" }, "dependencies": { - "@vx/group": "0.0.114", + "@vx/group": "0.0.120", "classnames": "^2.2.5", "prop-types": "^15.5.10" } -} +} \ No newline at end of file From b1091c3d2c41545ae62e3c267ee3044e6b8ea1c9 Mon Sep 17 00:00:00 2001 From: Harry Shoff Date: Tue, 27 Jun 2017 22:33:03 -0700 Subject: [PATCH 6/6] [demo] add legends tile --- packages/vx-demo/components/gallery.js | 308 +++++++++---------- packages/vx-demo/components/tiles/legends.js | 212 +++++++++++++ packages/vx-demo/pages/legends.js | 294 +++++++++--------- 3 files changed, 503 insertions(+), 311 deletions(-) create mode 100644 packages/vx-demo/components/tiles/legends.js diff --git a/packages/vx-demo/components/gallery.js b/packages/vx-demo/components/gallery.js index f59922476..8536278f5 100644 --- a/packages/vx-demo/components/gallery.js +++ b/packages/vx-demo/components/gallery.js @@ -22,20 +22,21 @@ import LineRadial from '../components/tiles/lineradial'; import Arcs from '../components/tiles/arc'; import Trees from '../components/tiles/tree'; import Cluster from '../components/tiles/dendrogram'; +import Legends from '../components/tiles/legends'; const items = [ - "#242424", - "#c3dae8", - "#ef5843", - "#f5f2e3", - "#f6c431", - "#32deaa", - "rgba(243, 129, 129, 1.000)", - "#00f2ff", - "#f4419f", - "#3130e3", - "#12122e", - "#ff657c" + '#242424', + '#c3dae8', + '#ef5843', + '#f5f2e3', + '#f6c431', + '#32deaa', + 'rgba(243, 129, 129, 1.000)', + '#00f2ff', + '#f4419f', + '#3130e3', + '#12122e', + '#ff657c', ]; export default class Gallery extends React.Component { @@ -59,7 +60,7 @@ export default class Gallery extends React.Component { resize() { const newState = []; - this.nodes.forEach((node) => { + this.nodes.forEach(node => { if (!node) return; newState.push([node.offsetWidth, node.clientHeight]); }); @@ -86,10 +87,7 @@ export default class Gallery extends React.Component { return (
- +
this.nodes.add(d)} >
- +
Lines
@@ -111,10 +106,7 @@ export default class Gallery extends React.Component {
- +
this.nodes.add(d)} >
- +
Bars
@@ -136,10 +125,7 @@ export default class Gallery extends React.Component {
- +
this.nodes.add(d)} >
- +
Dots
@@ -161,10 +144,7 @@ export default class Gallery extends React.Component {
- +
this.nodes.add(d)} >
- +
Patterns
@@ -186,10 +163,7 @@ export default class Gallery extends React.Component {
- +
- +
-
+
Stacked Areas
{``}
@@ -248,17 +222,18 @@ export default class Gallery extends React.Component {
- + -
this.nodes.add(d)}> +
this.nodes.add(d)} + >
- +
Gradients
@@ -269,12 +244,13 @@ export default class Gallery extends React.Component {
- + -
this.nodes.add(d)}> +
this.nodes.add(d)} + >
-
+
Glyphs
{``}
@@ -296,12 +275,13 @@ export default class Gallery extends React.Component {
- + -
this.nodes.add(d)}> +
this.nodes.add(d)} + >
-
+
Axis
-
{` + `}
+
{` + `}
- + -
this.nodes.add(d)}> +
this.nodes.add(d)} + >
- +
-
+
Bar Group
{``}
@@ -344,19 +323,17 @@ export default class Gallery extends React.Component {
- + -
this.nodes.add(d)}> +
this.nodes.add(d)} + >
- +
-
+
Bar Stack
{``}
@@ -365,19 +342,20 @@ export default class Gallery extends React.Component {
- + -
this.nodes.add(d)}> +
this.nodes.add(d)} + >
- +
-
+
Heatmaps
{` + `}
@@ -386,19 +364,17 @@ export default class Gallery extends React.Component {
- + -
this.nodes.add(d)}> +
this.nodes.add(d)} + >
- +
-
+
Radial Lines
{``}
@@ -407,19 +383,17 @@ export default class Gallery extends React.Component {
- + -
this.nodes.add(d)}> +
this.nodes.add(d)} + >
- +
-
+
Arcs
{``}
@@ -428,54 +402,70 @@ export default class Gallery extends React.Component {
- + -
this.nodes.add(d)}> +
this.nodes.add(d)} + >
- +
-
+
Trees
-
{` + `}
+
{` + `}
- + -
this.nodes.add(d)}> +
this.nodes.add(d)} + >
- +
-
+
Dendrograms
-
{` + `}
+
{` + `}
+
+
+
+ + + + +
this.nodes.add(d)} + > +
+ +
+
+
Legends
+
+
{``}
-
-

+

More on the way!

@@ -506,7 +496,9 @@ export default class Gallery extends React.Component { flex-direction: column; border-radius: 14px; } - .gallery-item.placeholder { height: 1px; } + .gallery-item.placeholder { + height: 1px; + } .image { flex: 1; display: flex; @@ -527,16 +519,20 @@ export default class Gallery extends React.Component { pre { margin: 0; } - .color-blue { color: rgba(25, 231, 217, 1.000); } - .color-yellow { color: #f6c431; } - .color-gray { color: #333; } - + .color-blue { + color: rgba(25, 231, 217, 1.000); + } + .color-yellow { + color: #f6c431; + } + .color-gray { + color: #333; + } @media (max-width: 960px) { .gallery-item { min-width: 45%; } } - @media (max-width: 600px) { .gallery-item { min-width: 100%; diff --git a/packages/vx-demo/components/tiles/legends.js b/packages/vx-demo/components/tiles/legends.js new file mode 100644 index 000000000..bafc81fb7 --- /dev/null +++ b/packages/vx-demo/components/tiles/legends.js @@ -0,0 +1,212 @@ +import React from 'react'; +import { format } from 'd3-format'; +import { + Legend, + LegendLinear, + LegendQuantile, + LegendOrdinal, + LegendSize, + LegendThreshold, +} from '@vx/legend'; +import { + scaleQuantize, + scaleLinear, + scaleOrdinal, + scaleThreshold, +} from '@vx/scale'; + +const oneDecimalFormat = format('.1f'); +const twoDecimalFormat = format('.2f'); + +const quantile = scaleQuantize({ + domain: [0, 0.15], + range: ['#fdd0a2', '#fdae6b', '#fd8d3c', '#f16913', '#d94801'], +}); + +const linear = scaleLinear({ + domain: [0, 10], + range: ['#0068af', '#c00029'], +}); + +const ordinalColor = scaleOrdinal({ + domain: ['a', 'b', 'c', 'd'], + range: ['#160689', '#a72297', '#f68e44', '#f8e126'].reverse(), +}); + +const ordinalShape = scaleOrdinal({ + domain: ['a', 'b', 'c', 'd'], + range: [ + props => + , + props => , + props => + , + props => + + $ + , + ], +}); + +const threshold = scaleThreshold({ + domain: [0.02, 0.04, 0.06, 0.08, 0.1], + range: [ + '#f2f0f7', + '#dadaeb', + '#bcbddc', + '#9e9ac8', + '#756bb1', + '#54278f', + ], +}); + +const size = scaleLinear({ + domain: [0, 10], + range: [10, 30], +}); + +const sizeOpacity = scaleLinear({ + domain: [0, 10], + range: [0.4, 1], +}); + +function LegendDemo({ title, children }) { + return ( +
+
{title}
+ {children} + +
+ ); +} + +export default ({ width, height, margin }) => { + if (width < 10) return null; + return ( +
+
+ + { + return { + fill: '#b2212b', + fillOpacity: sizeOpacity(props.datum), + }; + }} + shape={props => { + const { size } = props; + return ( + + + + ); + }} + /> + + + + + + { + if (i % 2 === 0) return oneDecimalFormat(d); + return ''; + }} + /> + + + + + + ordinalColor(datum)} + labelFormat={label => `Type ${label.toUpperCase()}`} + /> + + + ordinalColor(datum)} + shape={props => { + return ( + + {React.createElement( + ordinalShape(props.label.datum), + { + ...props, + }, + )} + + ); + }} + /> + +
+ + +
+ ); +}; diff --git a/packages/vx-demo/pages/legends.js b/packages/vx-demo/pages/legends.js index 3fe9ba368..53b0aef41 100644 --- a/packages/vx-demo/pages/legends.js +++ b/packages/vx-demo/pages/legends.js @@ -1,13 +1,29 @@ +import React from 'react'; +import Show from '../components/show'; +import Legends from '../components/tiles/legends'; + +export default () => { + return ( + + {`import React from 'react'; import { format } from 'd3-format'; -import { Group } from '@vx/group'; -import { GlyphCross } from '@vx/glyph'; import { - LegendQuantile, + Legend, LegendLinear, + LegendQuantile, LegendOrdinal, - LegendThreshold, LegendSize, - Legend, + LegendThreshold, } from '@vx/legend'; import { scaleQuantize, @@ -19,21 +35,118 @@ import { const oneDecimalFormat = format('.1f'); const twoDecimalFormat = format('.2f'); + +// LegendQuantile const quantile = scaleQuantize({ domain: [0, 0.15], range: ['#fdd0a2', '#fdae6b', '#fd8d3c', '#f16913', '#d94801'], }); +function Quantile() { + return ( + + ); +} + +// LegendLinear const linear = scaleLinear({ domain: [0, 10], range: ['#0068af', '#c00029'], }); +function Linear() { + return ( + { + if (i % 2 === 0) return oneDecimalFormat(d); + return ''; + }} + /> + ); +} + +// LegendThreshold +const threshold = scaleThreshold({ + domain: [0.02, 0.04, 0.06, 0.08, 0.1], + range: [ + '#f2f0f7', + '#dadaeb', + '#bcbddc', + '#9e9ac8', + '#756bb1', + '#54278f', + ], +}); + +function Threshold() { + return ( + + ); +} + +// LegendOrdinal const ordinalColor = scaleOrdinal({ domain: ['a', 'b', 'c', 'd'], range: ['#160689', '#a72297', '#f68e44', '#f8e126'].reverse(), }); +function Ordinal() { + return ( + ordinalColor(datum)} + labelFormat={label => \`Type \${label.toUpperCase()}\`} + /> + ); +} + +// LegendSize +const size = scaleLinear({ + domain: [0, 10], + range: [10, 30], +}); +const sizeOpacity = scaleLinear({ + domain: [0, 10], + range: [0.4, 1], +}); + +function Size() { + { + return { + fill: '#b2212b', + fillOpacity: sizeOpacity(props.datum), + }; + }} + shape={props => { + const { size } = props; + return ( + + + + ); + }} + /> +} + +// Custom Legend const ordinalShape = scaleOrdinal({ domain: ['a', 'b', 'c', 'd'], range: [ @@ -63,158 +176,29 @@ const ordinalShape = scaleOrdinal({ ], }); -const threshold = scaleThreshold({ - domain: [0.02, 0.04, 0.06, 0.08, 0.1], - range: [ - '#f2f0f7', - '#dadaeb', - '#bcbddc', - '#9e9ac8', - '#756bb1', - '#54278f', - ], -}); - -const size = scaleLinear({ - domain: [0, 10], - range: [10, 30], -}); - -const sizeOpacity = scaleLinear({ - domain: [0, 10], - range: [0.4, 1], -}); - -const SizeItem = ({ width, height, scale, fill, label }) => { - const radius = Math.max(width, height) / 2; - return ( - - - - ); -}; - -function LegendDemo({ title, children }) { +function CustomLegend() { return ( -
-
{title}
- {children} - -
+ ordinalColor(datum)} + shape={props => { + const { width, height, label } = props; + const { datum } = label; + return ( + + {React.createElement(ordinalShape(datum), { ...props })} + + ); + }} + /> ); } - -export default () => { - return ( -
-
- - { - return { - fill: '#b2212b', - fillOpacity: sizeOpacity(props.datum), - }; - }} - shape={props => { - const { size } = props; - return ( - - - - ); - }} - /> - - - - - - { - if (i % 2 === 0) return oneDecimalFormat(d); - return ''; - }} - /> - - - - - - ordinalColor(datum)} - labelFormat={label => `Type ${label.toUpperCase()}`} - /> - - - ordinalColor(datum)} - shape={props => { - return ( - - {React.createElement( - ordinalShape(props.label.datum), - { - ...props, - }, - )} - - ); - }} - /> - -
- - -
+`} +
); };