diff --git a/.changeset/nine-cups-complain.md b/.changeset/nine-cups-complain.md new file mode 100644 index 000000000..346da3210 --- /dev/null +++ b/.changeset/nine-cups-complain.md @@ -0,0 +1,5 @@ +--- +"victory-candlestick": patch +--- + +Migrate victory-candlestick to TypeScript diff --git a/packages/victory-candlestick/package.json b/packages/victory-candlestick/package.json index 424f15a7e..c10952f60 100644 --- a/packages/victory-candlestick/package.json +++ b/packages/victory-candlestick/package.json @@ -28,7 +28,9 @@ "react": ">=16.6.0" }, "devDependencies": { - "victory-chart": "^36.8.1" + "victory-vendor": "^36.8.1", + "victory-chart": "^36.8.1", + "victory-candlestick": "*" }, "publishConfig": { "provenance": true @@ -174,8 +176,8 @@ "dependencies": [ "types:create", "../victory-core:types:create", - "../victory-chart:types:create", "../victory-vendor:types:create", + "../victory-chart:types:create", "../victory-voronoi:types:create" ], "output": [], @@ -240,8 +242,8 @@ "output": [], "dependencies": [ "../victory-core:types:create", - "../victory-chart:types:create", "../victory-vendor:types:create", + "../victory-chart:types:create", "../victory-voronoi:types:create" ], "packageLocks": [ @@ -258,8 +260,8 @@ "output": [], "dependencies": [ "../victory-core:types:create", - "../victory-chart:types:create", "../victory-vendor:types:create", + "../victory-chart:types:create", "../victory-voronoi:types:create" ], "packageLocks": [ @@ -277,8 +279,8 @@ "output": [], "dependencies": [ "build:lib:cjs", - "../victory-chart:build:lib:cjs", "../victory-vendor:build:lib:cjs", + "../victory-chart:build:lib:cjs", "../victory-voronoi:build:lib:cjs" ], "packageLocks": [ diff --git a/packages/victory-candlestick/src/candle.test.js b/packages/victory-candlestick/src/candle.test.tsx similarity index 95% rename from packages/victory-candlestick/src/candle.test.js rename to packages/victory-candlestick/src/candle.test.tsx index 523942ac7..b39cc5c13 100644 --- a/packages/victory-candlestick/src/candle.test.js +++ b/packages/victory-candlestick/src/candle.test.tsx @@ -50,7 +50,7 @@ describe("victory-primitives/candle", () => { wicks.forEach((wick, i) => { const [x1, x2, y1, y2] = ["x1", "x2", "y1", "y2"].map((prop) => - parseInt(wick.getAttribute(prop)), + parseInt(wick.getAttribute(prop) || ""), ); expect(values[i]).toMatchObject({ x1, x2, y1, y2 }); }); @@ -60,7 +60,7 @@ describe("victory-primitives/candle", () => { const { container } = renderCandle(); const rect = container.querySelector("rect"); const [width, height, x, y] = ["width", "height", "x", "y"].map((prop) => - rect.getAttribute(prop), + rect?.getAttribute(prop), ); // width = style.width || 0.5 * (width - 2 * padding) / data.length; diff --git a/packages/victory-candlestick/src/candle.js b/packages/victory-candlestick/src/candle.tsx similarity index 70% rename from packages/victory-candlestick/src/candle.js rename to packages/victory-candlestick/src/candle.tsx index ba8d591d2..adf51f1de 100644 --- a/packages/victory-candlestick/src/candle.js +++ b/packages/victory-candlestick/src/candle.tsx @@ -1,10 +1,34 @@ -/* eslint no-magic-numbers: ["error", { "ignore": [0, 0.5, 1, 2] }]*/ import React from "react"; -import PropTypes from "prop-types"; -import { Helpers, CommonProps, Line, Rect } from "victory-core"; +import { + Helpers, + Line, + NumberOrCallback, + Rect, + VictoryCommonPrimitiveProps, + VictoryStyleObject, +} from "victory-core"; import { assign, defaults, isFunction } from "lodash"; -const getCandleWidth = (candleWidth, props) => { +export interface CandleProps extends VictoryCommonPrimitiveProps { + candleRatio?: number; + candleWidth?: NumberOrCallback; + close?: number; + datum?: any; + groupComponent?: React.ReactElement; + high?: number; + lineComponent?: React.ReactElement; + low?: number; + open?: number; + rectComponent?: React.ReactElement; + wickStrokeWidth?: number; + width?: number; + x?: number; +} + +const getCandleWidth = ( + candleWidth: CandleProps["candleWidth"], + props: CandleProps, +) => { const { style } = props; if (candleWidth) { return isFunction(candleWidth) @@ -16,7 +40,7 @@ const getCandleWidth = (candleWidth, props) => { return candleWidth; }; -const getCandleProps = (props, style) => { +const getCandleProps = (props, style: VictoryStyleObject) => { const { id, x, close, open, horizontal, candleWidth } = props; const candleLength = Math.abs(close - open); return { @@ -29,7 +53,7 @@ const getCandleProps = (props, style) => { }; }; -const getHighWickProps = (props, style) => { +const getHighWickProps = (props, style: VictoryStyleObject) => { const { horizontal, high, open, close, x, id } = props; return { key: `${id}-highWick`, @@ -41,7 +65,7 @@ const getHighWickProps = (props, style) => { }; }; -const getLowWickProps = (props, style) => { +const getLowWickProps = (props, style: VictoryStyleObject) => { const { horizontal, low, open, close, x, id } = props; return { key: `${id}-lowWick`, @@ -89,7 +113,7 @@ const evaluateProps = (props) => { }); }; -const defaultProps = { +const defaultProps: Partial = { groupComponent: , lineComponent: , rectComponent: , @@ -97,8 +121,8 @@ const defaultProps = { shapeRendering: "auto", }; -const Candle = (props) => { - props = evaluateProps({ ...defaultProps, ...props }); +export const Candle = (props: CandleProps) => { + const modifiedProps = evaluateProps({ ...defaultProps, ...props }); const { ariaLabel, events, @@ -114,7 +138,7 @@ const Candle = (props) => { style, desc, tabIndex, - } = props; + } = modifiedProps; const wickStyle = defaults({ strokeWidth: wickStrokeWidth }, style); const sharedProps = { ...events, @@ -127,9 +151,15 @@ const Candle = (props) => { desc, tabIndex, }; - const candleProps = assign(getCandleProps(props, style), sharedProps); - const highWickProps = assign(getHighWickProps(props, wickStyle), sharedProps); - const lowWickProps = assign(getLowWickProps(props, wickStyle), sharedProps); + const candleProps = assign(getCandleProps(modifiedProps, style), sharedProps); + const highWickProps = assign( + getHighWickProps(modifiedProps, wickStyle), + sharedProps, + ); + const lowWickProps = assign( + getLowWickProps(modifiedProps, wickStyle), + sharedProps, + ); return React.cloneElement(groupComponent, {}, [ React.cloneElement(rectComponent, candleProps), @@ -137,22 +167,3 @@ const Candle = (props) => { React.cloneElement(lineComponent, lowWickProps), ]); }; - -Candle.propTypes = { - ...CommonProps.primitiveProps, - candleRatio: PropTypes.number, - candleWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), - close: PropTypes.number, - datum: PropTypes.object, - groupComponent: PropTypes.element, - high: PropTypes.number, - lineComponent: PropTypes.element, - low: PropTypes.number, - open: PropTypes.number, - rectComponent: PropTypes.element, - wickStrokeWidth: PropTypes.number, - width: PropTypes.number, - x: PropTypes.number, -}; - -export default Candle; diff --git a/packages/victory-candlestick/src/helper-methods.test.js b/packages/victory-candlestick/src/helper-methods.test.ts similarity index 100% rename from packages/victory-candlestick/src/helper-methods.test.js rename to packages/victory-candlestick/src/helper-methods.test.ts diff --git a/packages/victory-candlestick/src/helper-methods.js b/packages/victory-candlestick/src/helper-methods.ts similarity index 96% rename from packages/victory-candlestick/src/helper-methods.js rename to packages/victory-candlestick/src/helper-methods.ts index e20cebd5e..128bc9e20 100644 --- a/packages/victory-candlestick/src/helper-methods.js +++ b/packages/victory-candlestick/src/helper-methods.ts @@ -6,10 +6,13 @@ import { Data, LabelHelpers, Collection, + VictoryStyleObject, } from "victory-core"; const TYPES = ["close", "open", "high", "low"]; +const DEFAULT_CANDLE_WIDTH = 8; + export const getData = (props) => { const accessorTypes = ["x", "high", "low", "close", "open"]; return Data.formatData(props.data, props, accessorTypes); @@ -58,7 +61,15 @@ const getLabelStyle = (props, styleObject, namespace) => { return defaults({}, tooltipTheme.style, baseStyle); }; -const getStyles = (props, style, defaultStyles = {}) => { +const getStyles = ( + props, + style, + defaultStyles: { + parent?: any; + labels?: any; + data?: any; + } = {}, +) => { if (props.disableInlineStyles) { return {}; } @@ -207,8 +218,8 @@ const getText = (props, type) => { return Array.isArray(labelProp) ? labelProp[index] : labelProp; }; -const getCandleWidth = (props, style) => { - const { data, candleWidth, scale, defaultCandleWidth } = props; +const getCandleWidth = (props, style?: VictoryStyleObject) => { + const { data, candleWidth, scale } = props; if (candleWidth) { return isFunction(candleWidth) ? Helpers.evaluateProp(candleWidth, props) @@ -221,7 +232,7 @@ const getCandleWidth = (props, style) => { const candles = data.length + 2; const candleRatio = props.candleRatio || 0.5; const defaultWidth = - candleRatio * (data.length < 2 ? defaultCandleWidth : extent / candles); + candleRatio * (data.length < 2 ? DEFAULT_CANDLE_WIDTH : extent / candles); return Math.max(1, defaultWidth); }; @@ -281,7 +292,7 @@ const calculatePlotValues = (props) => { /* eslint-enable complexity*/ /* eslint-disable max-params*/ -const getLabelProps = (props, text, style, type) => { +const getLabelProps = (props, text, style, type?: string) => { const { x, high, diff --git a/packages/victory-candlestick/src/index.d.ts b/packages/victory-candlestick/src/index.d.ts deleted file mode 100644 index d840bdf12..000000000 --- a/packages/victory-candlestick/src/index.d.ts +++ /dev/null @@ -1,110 +0,0 @@ -import * as React from "react"; -import { - EventPropTypeInterface, - OrientationTypes, - StringOrNumberOrCallback, - VictoryCommonProps, - VictoryCommonPrimitiveProps, - VictoryDatableProps, - VictoryStyleObject, - VictoryLabelStyleObject, - VictoryLabelableProps, - VictoryMultiLabelableProps, -} from "victory-core"; - -export interface VictoryCandlestickStyleInterface { - close?: VictoryStyleObject; - closeLabels?: VictoryLabelStyleObject | VictoryLabelStyleObject[]; - data?: VictoryStyleObject; - high?: VictoryStyleObject; - highLabels?: VictoryLabelStyleObject | VictoryLabelStyleObject[]; - labels?: VictoryLabelStyleObject | VictoryLabelStyleObject[]; - low?: VictoryStyleObject; - lowLabels?: VictoryLabelStyleObject | VictoryLabelStyleObject[]; - open?: VictoryStyleObject; - openLabels?: VictoryLabelStyleObject | VictoryLabelStyleObject[]; - parent?: VictoryStyleObject; -} - -export type VictoryCandlestickLabelsType = - | (string | number)[] - | boolean - | ((datum: any) => number); - -export interface VictoryCandlestickProps - extends Omit, - VictoryDatableProps, - VictoryLabelableProps, - VictoryMultiLabelableProps { - candleColors?: { - positive?: string; - negative?: string; - }; - candleRatio?: number; - candleWidth?: number | Function; - close?: StringOrNumberOrCallback | string[]; - closeLabelComponent?: React.ReactElement; - closeLabels?: VictoryCandlestickLabelsType; - eventKey?: StringOrNumberOrCallback | string[]; - events?: EventPropTypeInterface< - | "data" - | "labels" - | "open" - | "openLabels" - | "close" - | "closeLabels" - | "low" - | "lowLabels" - | "high" - | "highLabels", - StringOrNumberOrCallback | string[] - >[]; - high?: StringOrNumberOrCallback | string[]; - highLabelComponenet?: React.ReactElement; - highLabels?: VictoryCandlestickLabelsType; - labelOrientation?: - | OrientationTypes - | { - open?: OrientationTypes; - close?: OrientationTypes; - low?: OrientationTypes; - high?: OrientationTypes; - }; - low?: StringOrNumberOrCallback | string[]; - lowLabelComponent?: React.ReactElement; - lowLabels?: VictoryCandlestickLabelsType; - open?: StringOrNumberOrCallback | string[]; - openLabelComponent?: React.ReactElement; - openLabels?: VictoryCandlestickLabelsType; - size?: number; - style?: VictoryCandlestickStyleInterface; - wickStrokeWidth?: number; -} - -/** - * VictoryCandlestick renders a dataset as a series of candlesticks. - * VictoryCandlestick can be composed with VictoryChart to create candlestick charts. - */ - -export class VictoryCandlestick extends React.Component< - VictoryCandlestickProps, - any -> {} - -export interface CandleProps extends VictoryCommonPrimitiveProps { - candleRatio?: number; - candleWidth?: number | Function; - close?: number; - datum?: any; - groupComponent?: React.ReactElement; - high?: number; - lineComponent?: React.ReactElement; - low?: number; - open?: number; - rectComponent?: React.ReactElement; - wickStrokeWidth?: number; - width?: number; - x?: number; -} - -export class Candle extends React.Component {} diff --git a/packages/victory-candlestick/src/index.js b/packages/victory-candlestick/src/index.js deleted file mode 100644 index 27746185c..000000000 --- a/packages/victory-candlestick/src/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as VictoryCandlestick } from "./victory-candlestick"; -export { default as Candle } from "./candle"; diff --git a/packages/victory-candlestick/src/index.ts b/packages/victory-candlestick/src/index.ts new file mode 100644 index 000000000..63f66ba9d --- /dev/null +++ b/packages/victory-candlestick/src/index.ts @@ -0,0 +1,2 @@ +export * from "./victory-candlestick"; +export * from "./candle"; diff --git a/packages/victory-candlestick/src/victory-candlestick.js b/packages/victory-candlestick/src/victory-candlestick.js deleted file mode 100644 index c10e9a0b2..000000000 --- a/packages/victory-candlestick/src/victory-candlestick.js +++ /dev/null @@ -1,308 +0,0 @@ -import PropTypes from "prop-types"; -import React from "react"; -import { - PropTypes as CustomPropTypes, - Helpers, - VictoryLabel, - addEvents, - VictoryContainer, - VictoryTheme, - DefaultTransitions, - CommonProps, - UserProps, -} from "victory-core"; -import { isNil, flatten } from "lodash"; -import Candle from "./candle"; -import { getDomain, getData, getBaseProps } from "./helper-methods"; - -/* eslint-disable no-magic-numbers */ -const fallbackProps = { - width: 450, - height: 300, - padding: 50, - candleColors: { - positive: "#ffffff", - negative: "#252525", - }, -}; - -const options = { - components: [ - { name: "lowLabels" }, - { name: "highLabels" }, - { name: "openLabels" }, - { name: "closeLabels" }, - { name: "labels" }, - { name: "data" }, - { name: "parent", index: "parent" }, - ], -}; - -const defaultData = [ - { x: new Date(2016, 6, 1), open: 5, close: 10, high: 15, low: 0 }, - { x: new Date(2016, 6, 2), open: 10, close: 15, high: 20, low: 5 }, - { x: new Date(2016, 6, 3), open: 15, close: 20, high: 25, low: 10 }, - { x: new Date(2016, 6, 4), open: 20, close: 25, high: 30, low: 15 }, - { x: new Date(2016, 6, 5), open: 25, close: 30, high: 35, low: 20 }, - { x: new Date(2016, 6, 6), open: 30, close: 35, high: 40, low: 25 }, - { x: new Date(2016, 6, 7), open: 35, close: 40, high: 45, low: 30 }, - { x: new Date(2016, 6, 8), open: 40, close: 45, high: 50, low: 35 }, -]; -/* eslint-enable no-magic-numbers */ -const datumHasXandY = (datum) => { - return !isNil(datum._x) && !isNil(datum._y); -}; - -class VictoryCandlestick extends React.Component { - static animationWhitelist = [ - "data", - "domain", - "height", - "padding", - "samples", - "size", - "style", - "width", - ]; - - static displayName = "VictoryCandlestick"; - static role = "candlestick"; - static defaultTransitions = DefaultTransitions.discreteTransitions(); - - static propTypes = { - ...CommonProps.baseProps, - ...CommonProps.dataProps, - candleColors: PropTypes.shape({ - positive: PropTypes.string, - negative: PropTypes.string, - }), - candleRatio: PropTypes.number, - candleWidth: PropTypes.oneOfType([PropTypes.func, PropTypes.number]), - close: PropTypes.oneOfType([ - PropTypes.func, - CustomPropTypes.allOfType([ - CustomPropTypes.integer, - CustomPropTypes.nonNegative, - ]), - PropTypes.string, - PropTypes.arrayOf(PropTypes.string), - ]), - closeLabelComponent: PropTypes.element, - closeLabels: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.array, - PropTypes.bool, - ]), - events: PropTypes.arrayOf( - PropTypes.shape({ - target: PropTypes.oneOf([ - "data", - "labels", - "open", - "openLabels", - "close", - "closeLabels", - "low", - "lowLabels", - "high", - "highLabels", - ]), - eventKey: PropTypes.oneOfType([ - PropTypes.array, - CustomPropTypes.allOfType([ - CustomPropTypes.integer, - CustomPropTypes.nonNegative, - ]), - PropTypes.string, - ]), - eventHandlers: PropTypes.object, - }), - ), - high: PropTypes.oneOfType([ - PropTypes.func, - CustomPropTypes.allOfType([ - CustomPropTypes.integer, - CustomPropTypes.nonNegative, - ]), - PropTypes.string, - PropTypes.arrayOf(PropTypes.string), - ]), - highLabelComponent: PropTypes.element, - highLabels: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.array, - PropTypes.bool, - ]), - labelOrientation: PropTypes.oneOfType([ - PropTypes.oneOf(["top", "bottom", "left", "right"]), - PropTypes.shape({ - open: PropTypes.oneOf(["top", "bottom", "left", "right"]), - close: PropTypes.oneOf(["top", "bottom", "left", "right"]), - low: PropTypes.oneOf(["top", "bottom", "left", "right"]), - high: PropTypes.oneOf(["top", "bottom", "left", "right"]), - }), - ]), - low: PropTypes.oneOfType([ - PropTypes.func, - CustomPropTypes.allOfType([ - CustomPropTypes.integer, - CustomPropTypes.nonNegative, - ]), - PropTypes.string, - PropTypes.arrayOf(PropTypes.string), - ]), - lowLabelComponent: PropTypes.element, - lowLabels: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.array, - PropTypes.bool, - ]), - open: PropTypes.oneOfType([ - PropTypes.func, - CustomPropTypes.allOfType([ - CustomPropTypes.integer, - CustomPropTypes.nonNegative, - ]), - PropTypes.string, - PropTypes.arrayOf(PropTypes.string), - ]), - openLabelComponent: PropTypes.element, - openLabels: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.array, - PropTypes.bool, - ]), - style: PropTypes.shape({ - data: PropTypes.object, - labels: PropTypes.object, - close: PropTypes.object, - closeLabels: PropTypes.object, - open: PropTypes.object, - openLabels: PropTypes.object, - high: PropTypes.object, - highLabels: PropTypes.object, - low: PropTypes.object, - lowLabels: PropTypes.object, - }), - wickStrokeWidth: PropTypes.number, - }; - - static defaultProps = { - defaultCandleWidth: 8, - containerComponent: , - data: defaultData, - dataComponent: , - groupComponent: , - labelComponent: , - highLabelComponent: , - lowLabelComponent: , - openLabelComponent: , - closeLabelComponent: , - samples: 50, - sortOrder: "ascending", - standalone: true, - theme: VictoryTheme.grayscale, - }; - - static getDomain = getDomain; - static getData = getData; - static getBaseProps = (props) => getBaseProps(props, fallbackProps); - static expectedComponents = [ - "openLabelComponent", - "closeLabelComponent", - "highLabelComponent", - "lowLabelComponent", - "dataComponent", - "labelComponent", - "groupComponent", - "containerComponent", - ]; - - // Overridden in native versions - shouldAnimate() { - return !!this.props.animate; - } - - shouldRenderDatum(datum) { - return ( - !isNil(datum._x) && - !isNil(datum._high) && - !isNil(datum._low) && - !isNil(datum._close) && - !isNil(datum._open) - ); - } - - renderCandleData(props, shouldRenderDatum = datumHasXandY) { - const { dataComponent, labelComponent, groupComponent } = props; - const types = ["close", "open", "low", "high"]; - - const dataComponents = this.dataKeys.reduce( - (validDataComponents, _dataKey, index) => { - const dataProps = this.getComponentProps(dataComponent, "data", index); - if (shouldRenderDatum(dataProps.datum)) { - validDataComponents.push( - React.cloneElement(dataComponent, dataProps), - ); - } - return validDataComponents; - }, - [], - ); - - const labelComponents = flatten( - types.map((type) => { - const components = this.dataKeys.map((key, index) => { - const name = `${type}Labels`; - const baseComponent = props[`${type}LabelComponent`]; - const labelProps = this.getComponentProps(baseComponent, name, index); - if (labelProps.text !== undefined && labelProps.text !== null) { - return React.cloneElement(baseComponent, labelProps); - } - return undefined; - }); - return components.filter(Boolean); - }), - ); - - const labelsComponents = this.dataKeys - .map((_dataKey, index) => { - const labelProps = this.getComponentProps( - labelComponent, - "labels", - index, - ); - if (labelProps.text !== undefined && labelProps.text !== null) { - return React.cloneElement(labelComponent, labelProps); - } - return undefined; - }) - .filter(Boolean); - - const children = [ - ...dataComponents, - ...labelComponents, - ...labelsComponents, - ]; - return this.renderContainer(groupComponent, children); - } - - render() { - const { animationWhitelist, role } = VictoryCandlestick; - const props = Helpers.modifyProps(this.props, fallbackProps, role); - - if (this.shouldAnimate()) { - return this.animateComponent(props, animationWhitelist); - } - - const children = this.renderCandleData(props, this.shouldRenderDatum); - - const component = props.standalone - ? this.renderContainer(props.containerComponent, children) - : children; - - return UserProps.withSafeUserProps(component, props); - } -} - -export default addEvents(VictoryCandlestick, options); diff --git a/packages/victory-candlestick/src/victory-candlestick.test.js b/packages/victory-candlestick/src/victory-candlestick.test.tsx similarity index 94% rename from packages/victory-candlestick/src/victory-candlestick.test.js rename to packages/victory-candlestick/src/victory-candlestick.test.tsx index c3e6601d1..178285e37 100644 --- a/packages/victory-candlestick/src/victory-candlestick.test.js +++ b/packages/victory-candlestick/src/victory-candlestick.test.tsx @@ -47,14 +47,14 @@ describe("components/victory-candlestick", () => { 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 8 points", () => { @@ -100,7 +100,9 @@ describe("components/victory-candlestick", () => { , ); const candles = container.querySelectorAll("rect"); - const xValues = Array.from(candles).map((bar) => bar.getAttribute("x")); + const xValues = Array.from(candles).map((bar) => + Number(bar.getAttribute("x")), + ); const xValuesAscending = [...xValues].sort((a, b) => a - b); expect(xValues).toEqual(xValuesAscending); }); @@ -113,7 +115,9 @@ describe("components/victory-candlestick", () => { , ); const candles = container.querySelectorAll("rect"); - const xValues = Array.from(candles).map((bar) => bar.getAttribute("x")); + const xValues = Array.from(candles).map((bar) => + Number(bar.getAttribute("x")), + ); const xValuesDescending = [...xValues].sort((a, b) => b - a); expect(xValues).toEqual(xValuesDescending); }); @@ -155,6 +159,7 @@ describe("components/victory-candlestick", () => { it("renders data values with null accessor", () => { const data = range(10); const { container } = render( + // @ts-expect-error "'null' is not assignable to 'x'" { ariaLabel={({ datum }) => `open ${datum.open}, close ${datum.close}` } - tabIndex={({ index }) => index + 5} + tabIndex={({ index }) => Number(index) + 5} /> } />, diff --git a/packages/victory-candlestick/src/victory-candlestick.tsx b/packages/victory-candlestick/src/victory-candlestick.tsx new file mode 100644 index 000000000..73fead7da --- /dev/null +++ b/packages/victory-candlestick/src/victory-candlestick.tsx @@ -0,0 +1,305 @@ +import React from "react"; +import { + Helpers, + VictoryLabel, + addEvents, + VictoryContainer, + VictoryTheme, + DefaultTransitions, + UserProps, + StringOrNumberOrCallback, + EventPropTypeInterface, + OrientationTypes, + VictoryCommonProps, + VictoryDatableProps, + VictoryLabelStyleObject, + VictoryLabelableProps, + VictoryMultiLabelableProps, + VictoryStyleObject, + NumberOrCallback, + EventsMixinClass, +} from "victory-core"; +import { isNil } from "lodash"; +import { Candle } from "./candle"; +import { getDomain, getData, getBaseProps } from "./helper-methods"; + +export interface VictoryCandlestickStyleInterface { + close?: VictoryStyleObject; + closeLabels?: VictoryLabelStyleObject | VictoryLabelStyleObject[]; + data?: VictoryStyleObject; + high?: VictoryStyleObject; + highLabels?: VictoryLabelStyleObject | VictoryLabelStyleObject[]; + labels?: VictoryLabelStyleObject | VictoryLabelStyleObject[]; + low?: VictoryStyleObject; + lowLabels?: VictoryLabelStyleObject | VictoryLabelStyleObject[]; + open?: VictoryStyleObject; + openLabels?: VictoryLabelStyleObject | VictoryLabelStyleObject[]; + parent?: VictoryStyleObject; +} + +export type VictoryCandlestickLabelsType = + | (string | number)[] + | boolean + | ((datum: any) => number); + +export interface VictoryCandlestickProps + extends Omit, + VictoryDatableProps, + VictoryLabelableProps, + VictoryMultiLabelableProps { + candleColors?: { + positive?: string; + negative?: string; + }; + candleRatio?: number; + candleWidth?: NumberOrCallback; + close?: StringOrNumberOrCallback | string[]; + closeLabelComponent?: React.ReactElement; + closeLabels?: VictoryCandlestickLabelsType; + eventKey?: StringOrNumberOrCallback | string[]; + events?: EventPropTypeInterface< + | "data" + | "labels" + | "open" + | "openLabels" + | "close" + | "closeLabels" + | "low" + | "lowLabels" + | "high" + | "highLabels", + StringOrNumberOrCallback | string[] + >[]; + high?: StringOrNumberOrCallback | string[]; + highLabelComponent?: React.ReactElement; + highLabels?: VictoryCandlestickLabelsType; + labelOrientation?: + | OrientationTypes + | { + open?: OrientationTypes; + close?: OrientationTypes; + low?: OrientationTypes; + high?: OrientationTypes; + }; + low?: StringOrNumberOrCallback | string[]; + lowLabelComponent?: React.ReactElement; + lowLabels?: VictoryCandlestickLabelsType; + open?: StringOrNumberOrCallback | string[]; + openLabelComponent?: React.ReactElement; + openLabels?: VictoryCandlestickLabelsType; + size?: number; + style?: VictoryCandlestickStyleInterface; + wickStrokeWidth?: number; +} + +/* eslint-disable no-magic-numbers */ +const fallbackProps = { + width: 450, + height: 300, + padding: 50, + candleColors: { + positive: "#ffffff", + negative: "#252525", + }, +}; + +const options = { + components: [ + { name: "lowLabels" }, + { name: "highLabels" }, + { name: "openLabels" }, + { name: "closeLabels" }, + { name: "labels" }, + { name: "data" }, + { name: "parent", index: "parent" }, + ], +}; + +const defaultData = [ + { x: new Date(2016, 6, 1), open: 5, close: 10, high: 15, low: 0 }, + { x: new Date(2016, 6, 2), open: 10, close: 15, high: 20, low: 5 }, + { x: new Date(2016, 6, 3), open: 15, close: 20, high: 25, low: 10 }, + { x: new Date(2016, 6, 4), open: 20, close: 25, high: 30, low: 15 }, + { x: new Date(2016, 6, 5), open: 25, close: 30, high: 35, low: 20 }, + { x: new Date(2016, 6, 6), open: 30, close: 35, high: 40, low: 25 }, + { x: new Date(2016, 6, 7), open: 35, close: 40, high: 45, low: 30 }, + { x: new Date(2016, 6, 8), open: 40, close: 45, high: 50, low: 35 }, +]; +/* eslint-enable no-magic-numbers */ +const datumHasXandY = (datum) => { + return !isNil(datum._x) && !isNil(datum._y); +}; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface VictoryCandlestickBase + extends EventsMixinClass {} + +/** + * VictoryCandlestick renders a dataset as a series of candlesticks. + * VictoryCandlestick can be composed with VictoryChart to create candlestick charts. + */ +class VictoryCandlestickBase extends React.Component { + static animationWhitelist = [ + "data", + "domain", + "height", + "padding", + "samples", + "size", + "style", + "width", + ]; + + static displayName = "VictoryCandlestick"; + static role = "candlestick"; + static defaultTransitions = DefaultTransitions.discreteTransitions(); + + static defaultProps: VictoryCandlestickProps = { + containerComponent: , + data: defaultData, + dataComponent: , + groupComponent: , + labelComponent: , + highLabelComponent: , + lowLabelComponent: , + openLabelComponent: , + closeLabelComponent: , + samples: 50, + sortOrder: "ascending", + standalone: true, + theme: VictoryTheme.grayscale, + }; + + static getDomain = getDomain; + static getData = getData; + static getBaseProps = (props: VictoryCandlestickProps) => + getBaseProps(props, fallbackProps); + static expectedComponents = [ + "openLabelComponent", + "closeLabelComponent", + "highLabelComponent", + "lowLabelComponent", + "dataComponent", + "labelComponent", + "groupComponent", + "containerComponent", + ]; + + // Overridden in native versions + shouldAnimate() { + return !!this.props.animate; + } + + shouldRenderDatum = (datum) => { + return ( + !isNil(datum._x) && + !isNil(datum._high) && + !isNil(datum._low) && + !isNil(datum._close) && + !isNil(datum._open) + ); + }; + + renderCandleData( + props: VictoryCandlestickProps, + shouldRenderDatum = datumHasXandY, + ) { + const { dataComponent, labelComponent, groupComponent } = props; + const types = ["close", "open", "low", "high"]; + + if (!groupComponent) { + throw new Error("VictoryCandlestick expects a groupComponent prop"); + } + + const children: React.ReactElement[] = []; + + if (dataComponent) { + const dataComponents = this.dataKeys.reduce( + (validDataComponents, _dataKey, index) => { + const dataProps = this.getComponentProps( + dataComponent, + "data", + index, + ); + if (shouldRenderDatum((dataProps as any).datum)) { + validDataComponents.push( + React.cloneElement(dataComponent, dataProps), + ); + } + return validDataComponents; + }, + [], + ); + + children.push(...dataComponents); + } + + const labelComponents = types.flatMap((type) => + this.dataKeys + .map((key, index) => { + const name = `${type}Labels`; + const baseComponent: React.ReactElement = + props[`${type}LabelComponent`]; + const labelProps = this.getComponentProps(baseComponent, name, index); + if ( + (labelProps as any).text !== undefined && + (labelProps as any).text !== null + ) { + return React.cloneElement(baseComponent, labelProps); + } + return undefined; + }) + .filter( + (comp: React.ReactElement | undefined): comp is React.ReactElement => + comp !== undefined, + ), + ); + + children.push(...labelComponents); + + if (labelComponent) { + const labelsComponents = this.dataKeys + .map((_dataKey, index) => { + const labelProps = this.getComponentProps( + labelComponent, + "labels", + index, + ); + if ( + (labelProps as any).text !== undefined && + (labelProps as any).text !== null + ) { + return React.cloneElement(labelComponent, labelProps); + } + return undefined; + }) + .filter( + (comp: React.ReactElement | undefined): comp is React.ReactElement => + comp !== undefined, + ); + + children.push(...labelsComponents); + } + + return this.renderContainer(groupComponent, children); + } + + render(): React.ReactElement { + const { animationWhitelist, role } = VictoryCandlestick; + const props = Helpers.modifyProps(this.props, fallbackProps, role); + + if (this.shouldAnimate()) { + return this.animateComponent(props, animationWhitelist); + } + + const children = this.renderCandleData(props, this.shouldRenderDatum); + + const component = props.standalone + ? this.renderContainer(props.containerComponent, children) + : children; + + return UserProps.withSafeUserProps(component, props); + } +} + +export const VictoryCandlestick = addEvents(VictoryCandlestickBase, options); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a059dcf9..a18a57562 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -349,14 +349,18 @@ importers: specifiers: lodash: ^4.17.19 prop-types: ^15.8.1 + victory-candlestick: '*' victory-chart: ^36.8.1 victory-core: ^36.8.1 + victory-vendor: ^36.8.1 dependencies: lodash: 4.17.21 prop-types: 15.8.1 victory-core: link:../victory-core devDependencies: + victory-candlestick: 'link:' victory-chart: link:../victory-chart + victory-vendor: link:../victory-vendor packages/victory-canvas: specifiers: