diff --git a/.changeset/six-donuts-swim.md b/.changeset/six-donuts-swim.md new file mode 100644 index 000000000..a0145d8f0 --- /dev/null +++ b/.changeset/six-donuts-swim.md @@ -0,0 +1,5 @@ +--- +"victory-histogram": patch +--- + +Migrate victory-histogram to TypeScript diff --git a/packages/victory-histogram/package.json b/packages/victory-histogram/package.json index cf7895f77..be74435f3 100644 --- a/packages/victory-histogram/package.json +++ b/packages/victory-histogram/package.json @@ -27,6 +27,9 @@ "victory-core": "^36.8.2", "victory-vendor": "^36.8.2" }, + "devDependencies": { + "victory-histogram": "*" + }, "peerDependencies": { "react": ">=16.6.0" }, diff --git a/packages/victory-histogram/src/helper-methods.js b/packages/victory-histogram/src/helper-methods.ts similarity index 72% rename from packages/victory-histogram/src/helper-methods.js rename to packages/victory-histogram/src/helper-methods.ts index 0947ab540..1ca648105 100644 --- a/packages/victory-histogram/src/helper-methods.js +++ b/packages/victory-histogram/src/helper-methods.ts @@ -4,6 +4,7 @@ import { getBarPosition } from "victory-bar"; import isEqual from "react-fast-compare"; import * as d3Array from "victory-vendor/d3-array"; import * as d3Scale from "victory-vendor/d3-scale"; +import { VictoryHistogramProps } from "./victory-histogram"; const cacheLastValue = (func) => { let called = false; @@ -25,9 +26,14 @@ const cacheLastValue = (func) => { }; }; -const dataOrBinsContainDates = ({ data, bins, x }) => { +const dataOrBinsContainDates = ({ + data, + bins, + x, +}: Pick) => { const xAccessor = Helpers.createAccessor(x || "x"); - const dataIsDates = data.some((datum) => xAccessor(datum) instanceof Date); + const dataIsDates = + data?.some((datum) => xAccessor(datum) instanceof Date) || false; const binsHasDates = Array.isArray(bins) && bins.some((bin) => bin instanceof Date); @@ -39,7 +45,9 @@ const getBinningFunc = ({ data, x, bins, dataOrBinsContainsDates }) => { const bin = d3Array.bin().value(xAccessor); const niceScale = ( - dataOrBinsContainsDates ? d3Scale.scaleTime() : d3Scale.scaleLinear() + (dataOrBinsContainsDates + ? d3Scale.scaleTime() + : d3Scale.scaleLinear()) as any ) .domain(d3Array.extent(data, xAccessor)) .nice(); @@ -61,8 +69,7 @@ const getBinningFunc = ({ data, x, bins, dataOrBinsContainsDates }) => { if (dataOrBinsContainsDates) { bin.domain(niceScale.domain()); bin.thresholds(niceScale.ticks()); - - return bin; + return bin as unknown as d3Array.HistogramGeneratorDate; } bin.domain(niceScale.domain()); @@ -70,42 +77,52 @@ const getBinningFunc = ({ data, x, bins, dataOrBinsContainsDates }) => { return bin; }; -export const getFormattedData = cacheLastValue(({ data = [], x, bins }) => { - if ((!data || !data.length) && !Array.isArray(bins)) { - return []; - } - const dataOrBinsContainsDates = dataOrBinsContainDates({ data, bins, x }); - const binFunc = getBinningFunc({ data, x, bins, dataOrBinsContainsDates }); - const foo = binFunc(data); - const binnedData = foo.filter(({ x0, x1 }) => { - if (dataOrBinsContainsDates) { - return new Date(x0).getTime() !== new Date(x1).getTime(); +export const getFormattedData = cacheLastValue( + ({ + data = [], + x, + bins, + }: Pick) => { + if ((!data || !data.length) && !Array.isArray(bins)) { + return []; } - - return x0 !== x1; - }); - - const formattedData = binnedData.map((bin) => { - const x0 = dataOrBinsContainsDates ? new Date(bin.x0) : bin.x0; - const x1 = dataOrBinsContainsDates ? new Date(bin.x1) : bin.x1; - - return { - x0, - x1, - x: dataOrBinsContainsDates - ? new Date((x0.getTime() + x1.getTime()) / 2) - : (x0 + x1) / 2, - y: bin.length, - binnedData: [...bin], - }; - }); - - return formattedData; -}); - -export const getData = (props) => { + const dataOrBinsContainsDates = dataOrBinsContainDates({ data, bins, x }); + const binFunc = getBinningFunc({ data, x, bins, dataOrBinsContainsDates }); + const foo = binFunc(data); + const binnedData = [...foo].filter(({ x0, x1 }) => { + if (x0 instanceof Date && x1 instanceof Date) { + return new Date(x0).getTime() !== new Date(x1).getTime(); + } + + return x0 !== x1; + }); + + const formattedData = binnedData.map((bin) => { + const x0 = dataOrBinsContainsDates + ? new Date(bin.x0 as Date) + : bin.x0 || 0; + const x1 = dataOrBinsContainsDates + ? new Date(bin.x1 as Date) + : bin.x1 || 0; + + return { + x0, + x1, + x: dataOrBinsContainsDates + ? new Date(((x0 as Date).getTime() + (x1 as Date).getTime()) / 2) + : ((x0 as number) + (x1 as number)) / 2, + y: bin.length, + binnedData: [...bin], + }; + }); + + return formattedData; + }, +); + +export const getData = (props: VictoryHistogramProps) => { const { bins, data, x } = props; - const dataIsPreformatted = data.some(({ _y }) => !isNil(_y)); + const dataIsPreformatted = data?.some(({ _y }) => !isNil(_y)); const formattedData = dataIsPreformatted ? data @@ -113,7 +130,7 @@ export const getData = (props) => { return Data.getData({ ...props, data: formattedData, x: "x" }); }; -export const getDomain = (props, axis) => { +export const getDomain = (props: VictoryHistogramProps, axis: "x" | "y") => { const data = getData(props); if (!data.length) { @@ -130,7 +147,7 @@ export const getDomain = (props, axis) => { ); } - return props.data.length + return props.data?.length ? Domain.getDomainWithZero({ ...props, data }, "y") : [0, 1]; }; diff --git a/packages/victory-histogram/src/index.d.ts b/packages/victory-histogram/src/index.d.ts deleted file mode 100644 index d270ecb3b..000000000 --- a/packages/victory-histogram/src/index.d.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from "react"; -import { - EventPropTypeInterface, - NumberOrCallback, - StringOrNumberOrCallback, - VictoryCommonProps, - VictoryDatableProps, - VictoryMultiLabelableProps, - VictoryStyleInterface, -} from "victory-core"; - -export type VictoryHistogramTargetType = "data" | "labels" | "parent"; - -export interface VictoryHistogramProps - extends Omit, - Omit, - VictoryMultiLabelableProps { - binSpacing?: number; - bins?: number | number[] | Date[]; - cornerRadius?: - | NumberOrCallback - | { - top?: NumberOrCallback; - topLeft?: NumberOrCallback; - topRight?: NumberOrCallback; - bottom?: NumberOrCallback; - bottomLeft?: NumberOrCallback; - bottomRight?: NumberOrCallback; - }; - events?: EventPropTypeInterface< - VictoryHistogramTargetType, - number | string | number[] | string[] - >[]; - eventKey?: StringOrNumberOrCallback; - horizontal?: boolean; - style?: VictoryStyleInterface; -} - -/** - * Draw SVG histogram charts with React. VictoryHistogram is a composable component, so it doesn't include axes - * Check out VictoryChart for complete histogram charts and more. - */ -export class VictoryHistogram extends React.Component< - VictoryHistogramProps, - any -> {} diff --git a/packages/victory-histogram/src/index.js b/packages/victory-histogram/src/index.js deleted file mode 100644 index b07699a1f..000000000 --- a/packages/victory-histogram/src/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as VictoryHistogram } from "./victory-histogram"; diff --git a/packages/victory-histogram/src/index.ts b/packages/victory-histogram/src/index.ts new file mode 100644 index 000000000..50988ea89 --- /dev/null +++ b/packages/victory-histogram/src/index.ts @@ -0,0 +1 @@ +export * from "./victory-histogram"; diff --git a/packages/victory-histogram/src/victory-histogram.test.js b/packages/victory-histogram/src/victory-histogram.test.tsx similarity index 96% rename from packages/victory-histogram/src/victory-histogram.test.js rename to packages/victory-histogram/src/victory-histogram.test.tsx index 253f2f6ad..4850914c7 100644 --- a/packages/victory-histogram/src/victory-histogram.test.js +++ b/packages/victory-histogram/src/victory-histogram.test.tsx @@ -21,14 +21,14 @@ describe("components/victory-histogram", () => { it("renders an svg with the correct width and height", () => { const { container } = render(); const svg = container.querySelector("svg"); - expect(svg.getAttribute("style")).toContain("width: 100%; height: 100%"); + expect(svg?.getAttribute("style")).toContain("width: 100%; height: 100%"); }); it("renders an svg with the correct viewBox", () => { const { container } = render(); const svg = container.querySelector("svg"); const viewBoxValue = `0 0 ${450} ${300}`; - expect(svg.getAttribute("viewBox")).toEqual(viewBoxValue); + expect(svg?.getAttribute("viewBox")).toEqual(viewBoxValue); }); it("renders 0 bars", () => { @@ -94,6 +94,7 @@ describe("components/victory-histogram", () => { render( } @@ -139,7 +140,7 @@ describe("components/victory-histogram", () => { />, ); const svg = container.querySelector("svg"); - fireEvent.click(svg); + fireEvent.click(svg!); expect(clickHandler).toHaveBeenCalled(); // the first argument is the standard event object diff --git a/packages/victory-histogram/src/victory-histogram.js b/packages/victory-histogram/src/victory-histogram.tsx similarity index 52% rename from packages/victory-histogram/src/victory-histogram.js rename to packages/victory-histogram/src/victory-histogram.tsx index 73d1a4ee9..2f2a3fc5b 100644 --- a/packages/victory-histogram/src/victory-histogram.js +++ b/packages/victory-histogram/src/victory-histogram.tsx @@ -1,15 +1,20 @@ import React from "react"; -import PropTypes from "prop-types"; import { Bar } from "victory-bar"; import { Helpers, VictoryLabel, VictoryContainer, VictoryTheme, - CommonProps, addEvents, - PropTypes as CustomPropTypes, UserProps, + EventPropTypeInterface, + NumberOrCallback, + StringOrNumberOrCallback, + VictoryCommonProps, + VictoryDatableProps, + VictoryMultiLabelableProps, + VictoryStyleInterface, + EventsMixinClass, } from "victory-core"; import { getBaseProps, @@ -18,7 +23,34 @@ import { getFormattedData, } from "./helper-methods"; -const fallbackProps = { +export type VictoryHistogramTargetType = "data" | "labels" | "parent"; + +export interface VictoryHistogramProps + extends Omit, + Omit, + VictoryMultiLabelableProps { + binSpacing?: number; + bins?: number | number[] | Date[]; + cornerRadius?: + | NumberOrCallback + | { + top?: NumberOrCallback; + topLeft?: NumberOrCallback; + topRight?: NumberOrCallback; + bottom?: NumberOrCallback; + bottomLeft?: NumberOrCallback; + bottomRight?: NumberOrCallback; + }; + events?: EventPropTypeInterface< + VictoryHistogramTargetType, + number | string | number[] | string[] + >[]; + eventKey?: StringOrNumberOrCallback; + horizontal?: boolean; + style?: VictoryStyleInterface; +} + +const fallbackProps: Partial = { width: 450, height: 300, padding: 50, @@ -26,7 +58,15 @@ const fallbackProps = { const defaultData = []; -export class VictoryHistogram extends React.Component { +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface VictoryHistogramBase + extends EventsMixinClass {} + +/** + * Draw SVG histogram charts with React. VictoryHistogram is a composable component, so it doesn't include axes + * Check out VictoryChart for complete histogram charts and more. + */ +class VictoryHistogramBase extends React.Component { static animationWhitelist = [ "data", "domain", @@ -59,33 +99,7 @@ export class VictoryHistogram extends React.Component { static getFormattedData = getFormattedData; - static propTypes = { - ...CommonProps.baseProps, - ...CommonProps.dataProps, - binSpacing: CustomPropTypes.nonNegative, - bins: PropTypes.oneOfType([ - PropTypes.arrayOf( - PropTypes.oneOfType([PropTypes.number, PropTypes.instanceOf(Date)]), - ), - CustomPropTypes.nonNegative, - ]), - cornerRadius: PropTypes.oneOfType([ - PropTypes.number, - PropTypes.func, - PropTypes.shape({ - top: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), - topLeft: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), - topRight: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), - bottom: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), - bottomLeft: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), - bottomRight: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), - }), - ]), - getPath: PropTypes.func, - horizontal: PropTypes.bool, - }; - - static defaultProps = { + static defaultProps: VictoryHistogramProps = { containerComponent: , data: defaultData, dataComponent: , @@ -99,8 +113,9 @@ export class VictoryHistogram extends React.Component { static getDomain = getDomain; static getData = getData; - static getBaseProps = (props) => getBaseProps(props, fallbackProps); - static expectedComponents = [ + static getBaseProps = (props: VictoryHistogramProps) => + getBaseProps(props, fallbackProps); + static expectedComponents: Partial[] = [ "dataComponent", "labelComponent", "groupComponent", @@ -112,8 +127,8 @@ export class VictoryHistogram extends React.Component { return !!this.props.animate; } - render() { - const { animationWhitelist, role } = VictoryHistogram; + render(): React.ReactElement { + const { animationWhitelist, role } = VictoryHistogramBase; const props = Helpers.modifyProps(this.props, fallbackProps, role); if (this.shouldAnimate()) { @@ -130,4 +145,4 @@ export class VictoryHistogram extends React.Component { } } -export default addEvents(VictoryHistogram); +export const VictoryHistogram = addEvents(VictoryHistogramBase); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54da5fd20..fcde3ca40 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -485,6 +485,7 @@ importers: react-fast-compare: ^3.2.0 victory-bar: ^36.8.2 victory-core: ^36.8.2 + victory-histogram: '*' victory-vendor: ^36.8.2 dependencies: lodash: 4.17.21 @@ -493,6 +494,8 @@ importers: victory-bar: link:../victory-bar victory-core: link:../victory-core victory-vendor: link:../victory-vendor + devDependencies: + victory-histogram: 'link:' packages/victory-legend: specifiers: