diff --git a/packages/2019-housing/src/components/HomeAppreciation/HomeAppreciationVisualization.js b/packages/2019-housing/src/components/HomeAppreciation/HomeAppreciationVisualization.js index 78ad455fe..b0b886244 100644 --- a/packages/2019-housing/src/components/HomeAppreciation/HomeAppreciationVisualization.js +++ b/packages/2019-housing/src/components/HomeAppreciation/HomeAppreciationVisualization.js @@ -33,8 +33,8 @@ const HomeAppreciationVisualization = ({ data }) => { const lineChartData = data.annualHomeAppreciation.value.results.flatMap( yearData => [ { - series: "raw_appreciation_med", - value: yearData.raw_appreciation_med, + series: "raw_appreciation_med", // make this match the dataSeriesLabels + value: yearData.raw_appreciation_med, // make this match the dataSeriesLabels sale_year: yearData.sale_year }, { @@ -91,12 +91,14 @@ const HomeAppreciationVisualization = ({ data }) => { xLabel="Home Ownership Rate" yLabel="Race" dataValueFormatter={x => civicFormat.percentage(x)} + protect /> LineChart Visualization TODO: @@ -112,6 +114,7 @@ const HomeAppreciationVisualization = ({ data }) => { yLabel="Appreciation ($)" xNumberFormatter={x => civicFormat.year(x)} yNumberFormatter={y => civicFormat.dollars(y)} + protect /> Map Visualization TODO: diff --git a/packages/2019-housing/src/components/HomeOwnershipRates/HomeOwnershipRatesVisualization.js b/packages/2019-housing/src/components/HomeOwnershipRates/HomeOwnershipRatesVisualization.js index a9a6cd043..c5e73d5b9 100644 --- a/packages/2019-housing/src/components/HomeOwnershipRates/HomeOwnershipRatesVisualization.js +++ b/packages/2019-housing/src/components/HomeOwnershipRates/HomeOwnershipRatesVisualization.js @@ -33,6 +33,7 @@ const HomeOwnershipRatesVisualization = ({ data }) => { yLabel="Home Ownership Rate" xNumberFormatter={x => civicFormat.year(x)} yNumberFormatter={y => civicFormat.decimalToPercent(y)} + protect /> ) ); diff --git a/packages/2019-housing/src/components/HousingDisplacement/HousingDisplacementVisualization.js b/packages/2019-housing/src/components/HousingDisplacement/HousingDisplacementVisualization.js index c861d6f9b..bd3d153f8 100644 --- a/packages/2019-housing/src/components/HousingDisplacement/HousingDisplacementVisualization.js +++ b/packages/2019-housing/src/components/HousingDisplacement/HousingDisplacementVisualization.js @@ -82,6 +82,7 @@ const HousingDisplacementVisualization = ({ isLoading, data }) => { xLabel={xLabel} yLabel={yLabel} xNumberFormatter={tick => tick.toString()} + protect />
@@ -94,6 +95,7 @@ const HousingDisplacementVisualization = ({ isLoading, data }) => { xLabel={xLabel} yLabel={yLabel} xNumberFormatter={tick => tick.toString()} + protect />
diff --git a/packages/2019-template/src/components/DemoCard/DemoCardVisualization.js b/packages/2019-template/src/components/DemoCard/DemoCardVisualization.js index 7460b39a0..86a91184e 100644 --- a/packages/2019-template/src/components/DemoCard/DemoCardVisualization.js +++ b/packages/2019-template/src/components/DemoCard/DemoCardVisualization.js @@ -76,19 +76,18 @@ export default function DemoCardVisualization({ isLoading, data }) { Late Gentrification - {!isLoading && data[dataType] && ( - - )} + ); } diff --git a/packages/component-library/src/BarChart/BarChart.js b/packages/component-library/src/BarChart/BarChart.js index bb273b922..97d7eaec3 100644 --- a/packages/component-library/src/BarChart/BarChart.js +++ b/packages/component-library/src/BarChart/BarChart.js @@ -34,7 +34,8 @@ const BarChart = ({ error, theme }) => { - const chartDomain = domain || getDefaultDomain(data, dataKey, dataValue); + const safeData = data && data.length ? data : [{}]; + const chartDomain = domain || getDefaultDomain(safeData, dataKey, dataValue); return ( @@ -44,7 +45,7 @@ const BarChart = ({ loading={loading} error={error} > - + } - data={data.map(d => ({ + data={safeData.map(d => ({ dataKey: d[dataKey], dataValue: d[dataValue], label: `${xLabel}: ${xNumberFormatter( diff --git a/packages/component-library/src/ChartContainer/ChartContainer.js b/packages/component-library/src/ChartContainer/ChartContainer.js index b2f31016b..5d0cd39ab 100644 --- a/packages/component-library/src/ChartContainer/ChartContainer.js +++ b/packages/component-library/src/ChartContainer/ChartContainer.js @@ -1,14 +1,9 @@ import PropTypes from "prop-types"; /** @jsx jsx */ import { jsx, css } from "@emotion/core"; - +import { BrandColors } from "../index"; import ChartTitle from "../ChartTitle"; - -const chartLoading = css` - text-align: center; - background: #eee; - height: 100%; -`; +import Logo from "../Logo/Logo"; const chartError = css` text-align: center; @@ -16,6 +11,8 @@ const chartError = css` height: 100%; `; +const defaultVictoryAspectRatio = 650 / 350; + /** ChartContainer renders titles, subtitles, and provides some default styling for charts. It is designed to render a VictoryChart as children. @@ -29,7 +26,8 @@ const ChartContainer = ({ loading, subtitle, children, - className + className, + aspectRatio }) => { const figureWrapper = css` margin: 0; @@ -40,21 +38,42 @@ const ChartContainer = ({ width: 100%; ${className}; `; + const fullHeight = css` + position: relative; + padding-top: ${100 / aspectRatio}%; + height: 100%; + `; + + const skeletonStyle = css` + margin: 0 auto; + width: 100%; + max-width: 900px; + height: calc(100vw / ${aspectRatio}); + max-height: ${900 / aspectRatio}px; + background-color: ${BrandColors.subdued.hex}; + display: grid; + justify-items: center; + align-items: center; + `; let content = ( -
- -
{children}
-
+
+ +
); - if (loading) { - content =
Loading...
; + if (!loading) { + content =
{children}
; } else if (error) { - content =
{error}
; + content =
{error}
; } - return content; + return ( +
+ + {content} +
+ ); }; ChartContainer.propTypes = { @@ -63,9 +82,12 @@ ChartContainer.propTypes = { loading: PropTypes.bool, children: PropTypes.node, subtitle: PropTypes.string, - className: PropTypes.string + className: PropTypes.string, + aspectRatio: PropTypes.number }; -ChartContainer.defaultProps = {}; +ChartContainer.defaultProps = { + aspectRatio: defaultVictoryAspectRatio +}; export default ChartContainer; diff --git a/packages/component-library/src/HorizontalBarChart/HorizontalBarChart.js b/packages/component-library/src/HorizontalBarChart/HorizontalBarChart.js index 725d3d37f..26386171e 100644 --- a/packages/component-library/src/HorizontalBarChart/HorizontalBarChart.js +++ b/packages/component-library/src/HorizontalBarChart/HorizontalBarChart.js @@ -21,6 +21,7 @@ import { } from "../utils/chartHelpers"; import groupByKey from "../utils/groupByKey"; import DataChecker from "../utils/DataChecker"; +import protectData from "../utils/protectData"; import { VictoryTheme } from "../_Themes/index"; const HorizontalBarChart = ({ @@ -43,26 +44,35 @@ const HorizontalBarChart = ({ dataSeriesKey, dataSeriesLabel, legendComponent, - theme + theme, + protect }) => { + const safeData = + // eslint-disable-next-line no-nested-ternary + data && data.length + ? protect + ? protectData(data, { dataLabel, dataSeriesKey }) + : data + : [{}]; + const groupedDataIfStacked = () => { if (stacked) { if (hundredPercentData) { return transformDatato100( - groupByKey(data, dataSeriesKey, dataLabel), + groupByKey(safeData, dataSeriesKey, dataLabel), dataLabel, dataValue ); } - return groupByKey(data, dataSeriesKey, dataLabel); + return groupByKey(safeData, dataSeriesKey, dataLabel); } - return data; + return safeData; }; const groupedData = groupedDataIfStacked(); const barData = sortOrder && sortOrder.length - ? data - : data.map((d, index) => { + ? safeData + : safeData.map((d, index) => { return { ...d, defaultSort: index + 1 }; }); const sortOrderKey = @@ -73,13 +83,13 @@ const HorizontalBarChart = ({ const barHeight = theme.bar.style.data.width; const spaceHeight = theme.bar.style.data.padding * 2; const additionalHeight = padding.bottom + padding.top; - const minValue = Math.min(0, ...data.map(d => d[dataValue])); + const minValue = Math.min(0, ...safeData.map(d => d[dataValue])); - const bars = stacked ? groupedData.length : data.length; + const bars = stacked ? groupedData.length : safeData.length; const spaces = bars - 1; const dataHeight = bars * barHeight + spaces * spaceHeight; const dataSeriesLabels = dataSeriesKey - ? dataSeriesLabel || getDefaultDataSeriesLabels(data, dataSeriesKey) + ? dataSeriesLabel || getDefaultDataSeriesLabels(safeData, dataSeriesKey) : null; const legendData = dataSeriesLabels && dataSeriesLabels.length @@ -100,8 +110,9 @@ const HorizontalBarChart = ({ subtitle={subtitle} loading={loading} error={error} + aspectRatio={650 / (dataHeight + additionalHeight)} > - + {legendData && (legendComponent ? ( legendComponent(legendData) @@ -287,7 +298,8 @@ HorizontalBarChart.propTypes = { dataSeriesKey: PropTypes.string, dataSeriesLabel: PropTypes.shape({}), legendComponent: PropTypes.func, - theme: PropTypes.shape({}) + theme: PropTypes.shape({}), + protect: PropTypes.bool }; HorizontalBarChart.defaultProps = { @@ -308,7 +320,8 @@ HorizontalBarChart.defaultProps = { dataSeriesKey: null, dataSeriesLabel: {}, legendComponent: null, - theme: VictoryTheme + theme: VictoryTheme, + protect: false }; export default HorizontalBarChart; diff --git a/packages/component-library/src/HorizontalBarChart/HorizontalBarChart.test.js b/packages/component-library/src/HorizontalBarChart/HorizontalBarChart.test.js index e52a31057..fc8fe4098 100644 --- a/packages/component-library/src/HorizontalBarChart/HorizontalBarChart.test.js +++ b/packages/component-library/src/HorizontalBarChart/HorizontalBarChart.test.js @@ -18,6 +18,21 @@ const updatedSimpleData = [ { x: 500, y: "bat" } ]; +const simpleColorData = [ + { x: 100, y: "black" }, + { x: 200, y: "white" }, + { x: 300, y: "red" }, + { x: 400, y: "rat" } +]; + +const updatedSimpleColorData = [ + { x: 100, y: "black" }, + { x: 200, y: "white" }, + { x: 300, y: "red" }, + { x: 400, y: "rat" }, + { x: 500, y: "bat" } +]; + const simpleDataDomain = { x: [0, 400], y: [0, 4] }; const sampleUnstructuredData = [ @@ -148,6 +163,7 @@ describe("HorizontalBarChart", () => { const wrapper = shallow(); const dataProp = wrapper .find("VictoryBar") + .first() .prop("data"); expect(dataProp).to.have.length(4); @@ -166,4 +182,45 @@ describe("HorizontalBarChart", () => { expect(xProp).to.eql("sortOrder"); expect(yProp).to.eql("dataValue"); }); + + // Tests for https://github.com/FormidableLabs/victory/issues/928 workaround + + it("renders a HorizontalBarChart correctly with sample color data **invisible whitespace warning, see protectData util**", () => { + const c = "‌‌​"; // U+200B (zero-width space character) + const wrapper = shallow( + + ); + expect(wrapper.find({ title: "Horizontal Bar Chart" }).length).to.eql(1); + expect(wrapper.find({ title: "Horizontal Bar Chart" }).props().data).to.eql( + // invisible whitespace character injected in label before : + [ + { sortOrder: 1, dataValue: 100, label: `black${c}: 100` }, + { sortOrder: 2, dataValue: 200, label: `white${c}: 200` }, + { sortOrder: 3, dataValue: 300, label: `red${c}: 300` }, + { sortOrder: 4, dataValue: 400, label: `rat${c}: 400` } + ] + ); + }); + + it("renders an updated HorizontalBarChart when passed new data **invisible whitespace warning, see protectData util**", () => { + const c = "‌‌​"; // U+200B (zero-width space character) + const wrapper = shallow( + + ); + expect(wrapper.find({ title: "Horizontal Bar Chart" }).length).to.eql(1); + wrapper.setProps({ data: updatedSimpleColorData }); + + expect(wrapper.find({ title: "Horizontal Bar Chart" }).props().data).to.eql( + // invisible whitespace character injected in label before : + [ + { sortOrder: 1, dataValue: 100, label: `black${c}: 100` }, + { sortOrder: 2, dataValue: 200, label: `white${c}: 200` }, + { sortOrder: 3, dataValue: 300, label: `red${c}: 300` }, + { sortOrder: 4, dataValue: 400, label: `rat${c}: 400` }, + { sortOrder: 5, dataValue: 500, label: `bat${c}: 500` } + ] + ); + }); + + // End tests for https://github.com/FormidableLabs/victory/issues/928 workaround }); diff --git a/packages/component-library/src/LineChart/LineChart.js b/packages/component-library/src/LineChart/LineChart.js index 9f6d360c4..2c7750f6f 100644 --- a/packages/component-library/src/LineChart/LineChart.js +++ b/packages/component-library/src/LineChart/LineChart.js @@ -18,6 +18,7 @@ import { VictoryTheme } from "../_Themes/index"; import ChartContainer from "../ChartContainer"; import SimpleLegend from "../SimpleLegend"; import civicFormat from "../utils/civicFormat"; +import protectData from "../utils/protectData"; import DataChecker from "../utils/DataChecker"; import { chartEvents, @@ -45,12 +46,28 @@ const LineChart = ({ xNumberFormatter, yNumberFormatter, legendComponent, - theme + loading, + theme, + protect }) => { - const chartDomain = domain || getDefaultDomain(data, dataKey, dataValue); + const safeData = + // eslint-disable-next-line no-nested-ternary + data && data.length + ? protect + ? protectData(data, { dataSeries, dataSeriesLabel }) + : data + : [{}]; + const safeDataSeriesLabel = + // eslint-disable-next-line no-nested-ternary + dataSeriesLabel && dataSeriesLabel.length + ? protect + ? protectData(dataSeriesLabel, { x: "category", y: "label" }) + : dataSeriesLabel + : null; + const chartDomain = domain || getDefaultDomain(safeData, dataKey, dataValue); const dataSeriesLabels = dataSeries - ? dataSeriesLabel || getDefaultDataSeriesLabels(data, dataSeries) + ? safeDataSeriesLabel || getDefaultDataSeriesLabels(safeData, dataSeries) : null; const scatterPlotStyle = @@ -61,11 +78,16 @@ const LineChart = ({ ? dataSeriesLabels.map(series => ({ name: series.label })) : null; - const lineData = dataSeries ? groupBy(data, dataSeries) : { category: data }; + const lineData = dataSeries + ? groupBy(safeData, dataSeries) + : { category: safeData }; + // TODO: if you have line data with categories that don't have the same length, + // then the below won't work. const lines = lineData ? Object.keys(lineData).map((category, index) => ( ({ dataKey: d[dataKey], @@ -87,8 +109,8 @@ const LineChart = ({ return ( - - + + {legendData && (legendComponent ? ( legendComponent(legendData, theme) @@ -140,7 +162,7 @@ const LineChart = ({ {lines} ({ + data={safeData.map(d => ({ dataKey: d[dataKey], dataValue: d[dataValue], label: `${dataKeyLabel || xLabel}: ${xNumberFormatter( @@ -200,7 +222,9 @@ LineChart.propTypes = { xNumberFormatter: PropTypes.func, yNumberFormatter: PropTypes.func, legendComponent: PropTypes.func, - theme: PropTypes.shape({}) + theme: PropTypes.shape({}), + loading: PropTypes.bool, + protect: PropTypes.bool }; LineChart.defaultProps = { @@ -221,7 +245,9 @@ LineChart.defaultProps = { xNumberFormatter: civicFormat.numeric, yNumberFormatter: civicFormat.numeric, legendComponent: null, - theme: VictoryTheme + theme: VictoryTheme, + loading: null, + protect: false }; export default LineChart; diff --git a/packages/component-library/src/LineChart/LineChart.test.js b/packages/component-library/src/LineChart/LineChart.test.js index 55c2e6f27..8c35add3d 100644 --- a/packages/component-library/src/LineChart/LineChart.test.js +++ b/packages/component-library/src/LineChart/LineChart.test.js @@ -4,23 +4,146 @@ import { shallow } from "enzyme"; import LineChart from "./LineChart"; describe("LineChart", () => { - const data = [ - { year: 2015, population: 1 }, - { year: 2016, population: 2 }, - { year: 2017, population: 3 } + const simpleData = [{ x: 100, y: 10 }, { x: 200, y: 20 }]; + + const updatedSimpleData = [ + { x: 100, y: 10 }, + { x: 200, y: 20 }, + { x: 300, y: 30 } + ]; + + const simpleCategoryData = [ + { x: 100, y: 10, category: "cat" }, + { x: 200, y: 20, category: "cat" }, + { x: 100, y: 20, category: "dog" }, + { x: 200, y: 10, category: "dog" } + ]; + + const updatedSimpleCategoryData = [ + { x: 200, y: 20, category: "cat" }, + { x: 300, y: 30, category: "cat" }, + { x: 100, y: 20, category: "dog" }, + { x: 200, y: 10, category: "dog" }, + { x: 100, y: 10, category: "fish" }, + { x: 300, y: 25, category: "fish" } + ]; + + const simpleColorData = [ + { x: 100, y: 10, category: "black" }, + { x: 200, y: 20, category: "black" }, + { x: 100, y: 20, category: "white" }, + { x: 200, y: 10, category: "white" } ]; - const defaultProps = { - data, - dataKey: "year", - dataValue: "population" - }; + const updatedSimpleColorData = [ + { x: 200, y: 20, category: "black" }, + { x: 300, y: 30, category: "black" }, + { x: 100, y: 20, category: "white" }, + { x: 200, y: 10, category: "white" }, + { x: 100, y: 10, category: "fish" }, + { x: 300, y: 25, category: "fish" } + ]; it("should render a VictoryChart", () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find("VictoryChart").length).to.eql(1); }); + + it("renders a LineChart with sample data", () => { + const wrapper = shallow(); + expect(wrapper.find({ title: "Line Chart" }).length).to.eql(1); + expect(wrapper.find({ title: "Line Chart" }).props().data).to.eql([ + { dataKey: 100, dataValue: 10, series: undefined }, + { dataKey: 200, dataValue: 20, series: undefined } + ]); + }); + + it("renders an updated LineChart when passed new data", () => { + const wrapper = shallow(); + expect(wrapper.find({ title: "Line Chart" }).length).to.eql(1); + wrapper.setProps({ data: updatedSimpleData }); + + expect(wrapper.find({ title: "Line Chart" }).props().data).to.eql([ + { dataKey: 100, dataValue: 10, series: undefined }, + { dataKey: 200, dataValue: 20, series: undefined }, + { dataKey: 300, dataValue: 30, series: undefined } + ]); + }); + + it("renders a LineChart with sample category data", () => { + const wrapper = shallow( + + ); + expect(wrapper.find({ title: "Line Chart" }).length).to.eql(2); + expect( + wrapper + .find({ title: "Line Chart" }) + .first() + .props().data + ).to.eql([ + { dataKey: 100, dataValue: 10, series: "cat" }, + { dataKey: 200, dataValue: 20, series: "cat" } + ]); + }); + + it("renders an updated LineChart when passed new category data", () => { + const wrapper = shallow( + + ); + expect(wrapper.find({ title: "Line Chart" }).length).to.eql(2); + wrapper.setProps({ data: updatedSimpleCategoryData }); + expect(wrapper.find({ title: "Line Chart" }).length).to.eql(3); + + expect( + wrapper + .find({ title: "Line Chart" }) + .first() + .props().data + ).to.eql([ + { dataKey: 200, dataValue: 20, series: "cat" }, + { dataKey: 300, dataValue: 30, series: "cat" } + ]); + }); + + // Tests for https://github.com/FormidableLabs/victory/issues/928 workaround + + it("renders a LineChart with sample color category data", () => { + const c = "‌‌​"; // U+200B (zero-width space character) + const wrapper = shallow( + + ); + expect(wrapper.find({ title: "Line Chart" }).length).to.eql(2); + expect( + wrapper + .find({ title: "Line Chart" }) + .first() + .props().data + ).to.eql([ + { dataKey: 100, dataValue: 10, series: `black${c}` }, + { dataKey: 200, dataValue: 20, series: `black${c}` } + ]); + }); + + it("renders an updated LineChart when passed new color category data", () => { + const c = "‌‌​"; // U+200B (zero-width space character) + const wrapper = shallow( + + ); + expect(wrapper.find({ title: "Line Chart" }).length).to.eql(2); + wrapper.setProps({ data: updatedSimpleColorData }); + expect(wrapper.find({ title: "Line Chart" }).length).to.eql(3); + + expect( + wrapper + .find({ title: "Line Chart" }) + .first() + .props().data + ).to.eql([ + { dataKey: 200, dataValue: 20, series: `black${c}` }, + { dataKey: 300, dataValue: 30, series: `black${c}` } + ]); + }); /* TODO: rewrite these tests it('should render the relevant axis', () => { const wrapper = shallow(); diff --git a/packages/component-library/src/PieChart/PieChart.js b/packages/component-library/src/PieChart/PieChart.js index c08e1ca20..37f2f8d37 100644 --- a/packages/component-library/src/PieChart/PieChart.js +++ b/packages/component-library/src/PieChart/PieChart.js @@ -24,12 +24,14 @@ const PieChart = props => { theme } = props; + const safeData = data && data.length ? data : [{}]; const startAngle = halfDoughnut ? -90 : 0; const endAngle = halfDoughnut ? 90 : 360; const adjustedHeight = halfDoughnut ? height / 2 : height; - const legendLabels = data.map(value => ({ name: value[dataLabel] })); + const legendLabels = safeData.map(value => ({ name: value[dataLabel] })); const legendProps = {}; const colorScale = colors.length ? colors : theme.group.colorScale; + const aspectRatio = halfDoughnut ? 650 / 175 : 650 / 350; if (useLegend) { legendProps.labels = () => null; @@ -42,8 +44,9 @@ const PieChart = props => { subtitle={subtitle} loading={loading} error={error} + aspectRatio={aspectRatio} > - + {useLegend && ( { { - const chartDomain = domain || getDefaultDomain(data, dataKey, dataValue); + const safeData = + // eslint-disable-next-line no-nested-ternary + data && data.length + ? protect + ? protectData(data, { dataSeries, dataSeriesLabel }) + : data + : [{}]; + const safeDataSeriesLabel = + // eslint-disable-next-line no-nested-ternary + dataSeriesLabel && dataSeriesLabel.length + ? protect + ? protectData(dataSeriesLabel, { x: "category", y: "label" }) + : dataSeriesLabel + : null; + const chartDomain = domain || getDefaultDomain(safeData, dataKey, dataValue); const dataSeriesLabels = dataSeries - ? dataSeriesLabel || getDefaultDataSeriesLabels(data, dataSeries) + ? safeDataSeriesLabel || getDefaultDataSeriesLabels(safeData, dataSeries) : null; const scatterPlotStyle = @@ -75,7 +92,7 @@ const Scatterplot = ({ : null; return ( - + {legendData && (legendComponent ? ( legendComponent(legendData, theme) @@ -86,7 +103,7 @@ const Scatterplot = ({ theme={theme} /> ))} - + ({ + data={safeData.map(d => ({ dataKey: d[dataKey], dataValue: d[dataValue], label: `${ @@ -199,7 +216,9 @@ Scatterplot.propTypes = { invertX: PropTypes.bool, invertY: PropTypes.bool, legendComponent: PropTypes.func, - theme: PropTypes.shape({}) + theme: PropTypes.shape({}), + loading: PropTypes.bool, + protect: PropTypes.bool }; Scatterplot.defaultProps = { @@ -222,7 +241,9 @@ Scatterplot.defaultProps = { invertX: false, invertY: false, legendComponent: null, - theme: VictoryTheme + theme: VictoryTheme, + loading: null, + protect: false }; export default Scatterplot; diff --git a/packages/component-library/src/Scatterplot/Scatterplot.test.js b/packages/component-library/src/Scatterplot/Scatterplot.test.js index 02d2f40f5..ef232e1ea 100644 --- a/packages/component-library/src/Scatterplot/Scatterplot.test.js +++ b/packages/component-library/src/Scatterplot/Scatterplot.test.js @@ -35,6 +35,20 @@ const unstructuredMultiSeriesData = [ { amount: 200, rate: 3, type: "second" } ]; +const multiSeriesColorData = [ + { amount: 100, rate: 1, series: "black" }, + { amount: 200, rate: 2, series: "black" }, + { amount: 100, rate: 3, series: "white" }, + { amount: 200, rate: 3, series: "white" } +]; + +const unstructuredMultiSeriesColorData = [ + { amount: 100, rate: 1, type: "black" }, + { amount: 200, rate: 2, type: "black" }, + { amount: 100, rate: 3, type: "white" }, + { amount: 200, rate: 3, type: "white" } +]; + describe("Scatterplot", () => { it("renders a VictoryChart", () => { const wrapper = shallow(); @@ -160,6 +174,85 @@ describe("Scatterplot", () => { ]); }); + // Tests for https://github.com/FormidableLabs/victory/issues/928 workaround + it("renders multi-series color data", () => { + const c = "‌‌​"; // U+200B (zero-width space character) + const props = { + data: multiSeriesColorData, + dataKey: "amount", + dataValue: "rate", + dataSeries: "series", + protect: true + }; + const wrapper = shallow(); + expect(wrapper.find({ title: "Scatter Plot" }).props().data).to.eql([ + { + dataKey: 100, + dataValue: 1, + label: "X: 100 • Y: 1", + series: `black${c}` + }, + { + dataKey: 200, + dataValue: 2, + label: "X: 200 • Y: 2", + series: `black${c}` + }, + { + dataKey: 100, + dataValue: 3, + label: "X: 100 • Y: 3", + series: `white${c}` + }, + { + dataKey: 200, + dataValue: 3, + label: "X: 200 • Y: 3", + series: `white${c}` + } + ]); + }); + + it("renders unstructured multi-series color data", () => { + const c = "‌‌​"; // U+200B (zero-width space character) + const props = { + data: unstructuredMultiSeriesColorData, + dataKey: "amount", + dataValue: "rate", + dataSeries: "type", + protect: true + }; + const wrapper = shallow(); + expect(wrapper.find({ title: "Scatter Plot" }).props().data).to.eql([ + { + dataKey: 100, + dataValue: 1, + label: "X: 100 • Y: 1", + series: `black${c}` + }, + { + dataKey: 200, + dataValue: 2, + label: "X: 200 • Y: 2", + series: `black${c}` + }, + { + dataKey: 100, + dataValue: 3, + label: "X: 100 • Y: 3", + series: `white${c}` + }, + { + dataKey: 200, + dataValue: 3, + label: "X: 200 • Y: 3", + series: `white${c}` + } + ]); + }); + + // End tests for https://github.com/FormidableLabs/victory/issues/928 workaround + // TODO: make this test pass // it('sets the data point size if size key is specified', () => { diff --git a/packages/component-library/src/StackedAreaChart/StackedAreaChart.js b/packages/component-library/src/StackedAreaChart/StackedAreaChart.js index 7ba886915..920282980 100644 --- a/packages/component-library/src/StackedAreaChart/StackedAreaChart.js +++ b/packages/component-library/src/StackedAreaChart/StackedAreaChart.js @@ -44,13 +44,15 @@ const StackedAreaChart = ({ xNumberFormatter, yNumberFormatter, legendComponent, - theme + theme, + loading }) => { + const safeData = data && data.length ? data : [{}]; const chartDomain = - domain || getDefaultStackedDomain(data, dataKey, dataValue); + domain || getDefaultStackedDomain(safeData, dataKey, dataValue); const dataSeriesLabels = dataSeries - ? dataSeriesLabel || getDefaultDataSeriesLabels(data, dataSeries) + ? dataSeriesLabel || getDefaultDataSeriesLabels(safeData, dataSeries) : null; const scatterPlotStyle = style || { @@ -62,7 +64,9 @@ const StackedAreaChart = ({ ? dataSeriesLabels.map(series => ({ name: series.label })) : null; - const lineData = dataSeries ? groupBy(data, dataSeries) : { category: data }; + const lineData = dataSeries + ? groupBy(safeData, dataSeries) + : { category: safeData }; const areas = lineData ? Object.keys(lineData).map((category, index) => ( @@ -120,8 +124,8 @@ const StackedAreaChart = ({ : null; return ( - - + + {legendData && (legendComponent ? ( legendComponent(legendData, theme) @@ -203,7 +207,8 @@ StackedAreaChart.propTypes = { xNumberFormatter: PropTypes.func, yNumberFormatter: PropTypes.func, legendComponent: PropTypes.func, - theme: PropTypes.shape({}) + theme: PropTypes.shape({}), + loading: PropTypes.bool }; StackedAreaChart.defaultProps = { @@ -224,7 +229,8 @@ StackedAreaChart.defaultProps = { xNumberFormatter: civicFormat.numeric, yNumberFormatter: civicFormat.numeric, legendComponent: null, - theme: VictoryTheme + theme: VictoryTheme, + loading: null }; export default StackedAreaChart; diff --git a/packages/component-library/src/utils.js b/packages/component-library/src/utils.js index 3c3aa5044..6b5d5eee3 100644 --- a/packages/component-library/src/utils.js +++ b/packages/component-library/src/utils.js @@ -1,2 +1,3 @@ export { default as civicFormat } from "./utils/civicFormat"; export { default as ungroupBy } from "./utils/ungroupBy"; +export { default as protectData } from "./utils/protectData"; diff --git a/packages/component-library/src/utils/protectData.js b/packages/component-library/src/utils/protectData.js new file mode 100644 index 000000000..466221529 --- /dev/null +++ b/packages/component-library/src/utils/protectData.js @@ -0,0 +1,27 @@ +import { fromPairs } from "lodash"; + +const c = "‌‌​"; // U+200B (zero-width space character) + +/** + * This function protects data attributes from being accessed by d3-interpolate + * to work around https://github.com/FormidableLabs/victory/issues/928 + * + * @param {array} data - An array of data objects + * @param {object} dataAccessors - An object with { dataKey, dataValue, etc ..} + * @return {array} An array with each element transformed for each dataAcccessor to add a zero-width space character + */ + +function protectData(data, dataAccessors) { + return data.map(d => { + return { + ...d, + ...fromPairs( + Object.values(dataAccessors).map(property => { + return [property, `${d[property]}${c}`]; + }) + ) + }; + }); +} + +export default protectData; diff --git a/packages/component-library/stories/HorizontalBarChart.story.js b/packages/component-library/stories/HorizontalBarChart.story.js index 2c5c81f16..3faa75f26 100644 --- a/packages/component-library/stories/HorizontalBarChart.story.js +++ b/packages/component-library/stories/HorizontalBarChart.story.js @@ -118,6 +118,7 @@ export default () => { display: "select" }, GROUP_IDS.CUSTOM ); + const loading = boolean("Loading", false, GROUP_IDS.CUSTOM); return ( dataValueFormatter={x => civicFormat[optionSelectX](x)} minimalist={minimalist} theme={(name => themes[name])(theme)} + loading={loading} /> ); }, diff --git a/packages/component-library/stories/LineChart.story.js b/packages/component-library/stories/LineChart.story.js index 40b063bf1..307b2b1ef 100644 --- a/packages/component-library/stories/LineChart.story.js +++ b/packages/component-library/stories/LineChart.story.js @@ -7,7 +7,8 @@ import { text, number, withKnobs, - optionsKnob as options + optionsKnob as options, + boolean } from "@storybook/addon-knobs"; import { LineChart, @@ -250,6 +251,7 @@ export default () => { display: "select" }, GROUP_IDS.CUSTOM ); + const loading = boolean("Loading", false, GROUP_IDS.CUSTOM); return ( yNumberFormatter={y => civicFormat[optionSelectY](y)} legendComponent={customLegend} theme={(name => themes[name])(theme)} + loading={loading} /> ); }, diff --git a/packages/component-library/stories/PieChart.story.js b/packages/component-library/stories/PieChart.story.js index f3965c290..636758dc0 100644 --- a/packages/component-library/stories/PieChart.story.js +++ b/packages/component-library/stories/PieChart.story.js @@ -85,6 +85,7 @@ export default () => const chartHeight = number("Chart height", 350, {}, GROUP_IDS.CUSTOM); const chartWidth = number("Chart width", 650, {}, GROUP_IDS.CUSTOM); const innerRadius = number("Inner radius", 50, {}, GROUP_IDS.CUSTOM); + const loading = boolean("Loading", false, GROUP_IDS.CUSTOM); const themes = { VictoryTheme, VictoryCrazyTheme @@ -112,6 +113,7 @@ export default () => halfDoughnut={halfDoughnut} useLegend={useLegend} theme={(name => themes[name])(theme)} + loading={loading} /> ); }, diff --git a/packages/component-library/stories/Scatterplot.story.js b/packages/component-library/stories/Scatterplot.story.js index 9a2481fd3..04fc069df 100644 --- a/packages/component-library/stories/Scatterplot.story.js +++ b/packages/component-library/stories/Scatterplot.story.js @@ -273,6 +273,7 @@ export default () => { display: "select" }, GROUP_IDS.CUSTOM ); + const loading = boolean("Loading", false, GROUP_IDS.CUSTOM); return ( invertY={invertY} legendComponent={customLegend} theme={(name => themes[name])(theme)} + loading={loading} /> ); }, diff --git a/packages/component-library/stories/StackedAreaChart.story.js b/packages/component-library/stories/StackedAreaChart.story.js index 7de733320..3d88705fa 100644 --- a/packages/component-library/stories/StackedAreaChart.story.js +++ b/packages/component-library/stories/StackedAreaChart.story.js @@ -7,7 +7,8 @@ import { object, text, withKnobs, - optionsKnob as options + optionsKnob as options, + boolean } from "@storybook/addon-knobs"; import { StackedAreaChart, civicFormat, SimpleLegend } from "../src"; import { getKeyNames } from "./shared"; @@ -273,6 +274,7 @@ export default () => { display: "select" }, GROUP_IDS.CUSTOM ); + const loading = boolean("Loading", false, GROUP_IDS.CUSTOM); return ( yNumberFormatter={y => civicFormat[optionSelectY](y)} legendComponent={customLegend} theme={(name => themes[name])(theme)} + loading={loading} /> ); }, diff --git a/yarn.lock b/yarn.lock index 49c344bb0..3c3605d14 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5802,6 +5802,11 @@ d3-array@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.1.tgz#d1ca33de2f6ac31efadb8e050a021d7e2396d5dc" +d3-array@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.3.1.tgz#145cb03967578e675db8c62e41f3c406fa7acc73" + integrity sha512-YlOh8kwqIz0pDECEdCeqVNelaLQXznD0g6yidhhklMgKxKqbNDrYfoudLMkk9THlqvFll+pXMmXYAyN49yWsmg== + d3-axis@1: version "1.0.12" resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.12.tgz#cdf20ba210cfbb43795af33756886fb3638daac9"