diff --git a/demo/ts/app.tsx b/demo/ts/app.tsx index de643e125..5fca81a73 100644 --- a/demo/ts/app.tsx +++ b/demo/ts/app.tsx @@ -1,36 +1,8 @@ import * as React from "react"; import * as ReactDOM from "react-dom"; -import AccessibilityDemo from "./components/accessibility-demo"; -import BrushLineDemo from "./components/victory-brush-line-demo"; -import DraggableDemo from "./components/draggable-demo"; -import StackedThemeDemos from "./components/stacked-theme-demo"; import ThemeBuilder from "./components/theme-builder"; -const DEMO_ROUTES = { - "/demo/accessibility": { - component: AccessibilityDemo, - name: "AccessibilityDemo", - }, - "/demo/brush-line": { component: BrushLineDemo, name: "BrushLineDemo" }, - "/demo/draggable": { component: DraggableDemo, name: "DraggableDemo" }, - "/demo/stacked-theme": { - component: StackedThemeDemos, - name: "StackedThemeDemos", - }, -}; - -class Home extends React.Component { - render() { - return

Pick A Demo

; - } -} - -const NAV_ROUTES = { - "/demo": { component: Home, name: "Demos" }, - "/theme-builder": { component: ThemeBuilder, name: "ThemeBuilder" }, -}; - interface AppState { route: string; } @@ -42,26 +14,6 @@ const containerStyle: React.CSSProperties = { fontFamily: "sans-serif", }; -const navStyle: React.CSSProperties = { - display: "flex", - gap: "20px", - padding: "20px", - borderBottom: "1px solid #ddd", - fontWeight: "bold", - fontSize: "1.1em", - width: "100%", - height: 62, - top: 0, - background: "#fff", -}; - -const sidebarStyle: React.CSSProperties = { - flexShrink: "0", - borderRight: "1px solid #ddd", - overflow: "auto", - padding: "5px", -}; - const contentStyle: React.CSSProperties = { display: "flex", gap: "20px", @@ -69,126 +21,12 @@ const contentStyle: React.CSSProperties = { flex: 1, }; -const mainStyle: React.CSSProperties = { - overflow: "auto", - flex: 1, -}; - -const listStyle: React.CSSProperties = { - display: "flex", - flexDirection: "column", - listStyle: "none", - padding: "0", - margin: "0", - color: "#666", -}; - -const linkStyle: React.CSSProperties = { - color: "currentcolor", - textDecoration: "none", -}; - -const activeLinkStyle: React.CSSProperties = { - ...linkStyle, - color: "#2165E3", -}; - -const listItemStyle: React.CSSProperties = { - padding: "10px 15px", - borderRadius: "5px", - color: "#666", - margin: "5px 0", -}; - -const activeListItemStyle: React.CSSProperties = { - ...listItemStyle, - backgroundColor: "#eee", -}; - class App extends React.Component { - constructor(props: any) { - super(props); - - this.state = { - route: window.location.hash.slice(1), - }; - - if (this.state.route === "") { - window.location.hash = "#/demo"; - } - } - - componentDidMount() { - window.addEventListener("hashchange", () => { - this.setState({ - route: window.location.hash.substr(1), - }); - }); - } - - getChild() { - const item = - DEMO_ROUTES[this.state.route] || NAV_ROUTES[this.state.route] || {}; - return item.component || Home; - } - render() { - const Child = this.getChild(); - const demoRoutes = Object.keys(DEMO_ROUTES).sort(); - const navRoutes = Object.keys(NAV_ROUTES); - - const isDemoRoute = this.state.route.startsWith("/demo"); - return (
-
- {isDemoRoute ? ( - <> - -
- -
- - ) : ( - - )} +
); diff --git a/demo/ts/components/accessibility-demo.tsx b/demo/ts/components/accessibility-demo.tsx deleted file mode 100644 index d4abe1e37..000000000 --- a/demo/ts/components/accessibility-demo.tsx +++ /dev/null @@ -1,401 +0,0 @@ -import React from "react"; -import isNumber from "lodash/isNumber"; -import { VictoryGroup } from "victory-group"; -import { VictoryStack } from "victory-stack"; -import { VictoryChart } from "victory-chart"; -import { VictoryScatter } from "victory-scatter"; -import { VictoryBoxPlot } from "victory-box-plot"; -import { VictoryBar, Bar } from "victory-bar"; -import { VictoryPie, Slice } from "victory-pie"; -import { VictoryArea, Area } from "victory-area"; -import { VictoryLine, Curve } from "victory-line"; -import { VictoryVoronoi, Voronoi } from "victory-voronoi"; -import { ErrorBar, VictoryErrorBar } from "victory-errorbar"; -import { Candle, VictoryCandlestick } from "victory-candlestick"; -import { - LineSegment, - Whisker, - Border, - Point, - VictoryLabel, - VictoryAccessibleGroup, - VictoryTheme, -} from "victory-core"; -import { - accessibilityBarData, - accessibilityBoxData, - accessibilityPieData, - accessibilityAreaData, - accessibilityLineData, - accessibilityGroupData, - accessibilityScatterData, - accessibilityVoronoiData, - accessibilityErrorBarData, - accessibilityCandlestickData, -} from "../demo-data"; - -const pageHeadingStyle: React.CSSProperties = { - display: "flex", - flexDirection: "row", - width: "100%", - alignItems: "center", - justifyContent: "center", -}; - -const chartHeadingStyle: React.CSSProperties = { - marginBottom: "0px", - marginTop: "25px", - fontSize: "calc(1vw + 5px)", -}; - -const containerStyle: React.CSSProperties = { - display: "flex", - flexFlow: "row wrap", - alignItems: "center", - justifyContent: "flex-start", -}; - -const chartContainerStyle: React.CSSProperties = { - display: "flex", - flexDirection: "column", - alignItems: "center", - width: "50%", - height: "50%", - padding: "25px", -}; - -export const assignIndexValue = ( - index: number | string | undefined, - value: number, -): number => { - const determineValidNumber = Number(index); - return isNumber(determineValidNumber) ? determineValidNumber + value : 1; -}; - -export default class VictoryAccessibilityDemo extends React.Component { - render() { - return ( - <> -
-

Tabbable charts with aria-labels

-
-
- {/** BAR */} -
-

Bar chart

- - `x: ${datum.x}`} - tabIndex={({ index }) => assignIndexValue(index, 1)} - /> - } - /> - -
- - {/** BOXPLOT */} -
-

BoxPlot

- - `${datum.x} max is ${datum._max}`} - tabIndex={({ index }) => assignIndexValue(index, 5)} - /> - } - q3Component={ - - `${datum.x} q3 value is ${datum._q3}` - } - tabIndex={({ index }) => assignIndexValue(index, 6.1)} - /> - } - medianComponent={ - - `${datum.x} median value is ${datum._median}` - } - tabIndex={({ index }) => assignIndexValue(index, 5.1)} - /> - } - q1Component={ - - `${datum.x} q1 value is ${datum._q1}` - } - tabIndex={({ index }) => assignIndexValue(index, 6.2)} - /> - } - minComponent={ - `${datum.x} min is ${datum._min}`} - tabIndex={({ index }) => assignIndexValue(index, 5.2)} - /> - } - /> - -
- - {/** AREA */} -
-

Area

- - - } - > - - `area chart stack ${data?.[0]._stack}` - } - tabIndex={20} - /> - } - /> - - `area chart stack ${data?.[0]._stack}` - } - tabIndex={20.1} - /> - } - /> - - `area chart stack ${data?.[0]._stack}` - } - tabIndex={20.2} - /> - } - /> - - `area chart stack ${data?.[0]._stack}` - } - tabIndex={20.3} - /> - } - /> - - -
- - {/** LINE */} -
-

Line

- - datum.y} - labelComponent={ - datum.y} - tabIndex={({ index }) => assignIndexValue(index, 21)} - /> - } - dataComponent={ - - data - ?.map( - (dataPoint: any, i: number) => - `data point ${i + 1} x value is ${ - dataPoint.x - } and y value is ${dataPoint.y}`, - ) - .join("") || "" - } - /> - } - /> - -
- - {/** PIE */} -
-

Pie

- datum.radius - 12} - width={400} - height={250} - radius={({ datum }) => datum.radius} - data={accessibilityPieData} - dataComponent={ - `pie slice ${datum.x}`} - tabIndex={({ index }) => assignIndexValue(index, 30)} - /> - } - /> -
- - {/** SCATTER */} -
-

Scatter

- - - `scatter point x: ${datum.x}, y:${datum.y}` - } - tabIndex={({ index }) => assignIndexValue(index, 28)} - /> - } - /> - -
- - {/** VORONOI */} -
-

Voronoi

- - - `voronoi chart, x ${datum.x}, y ${datum.y}` - } - tabIndex={({ index }) => assignIndexValue(index, 35)} - /> - } - /> - -
- - {/** CANDLESTICK */} -
-

Candlestick

- - - `candlestick chart, ${datum.x} open ${datum.open}, close ${datum.close}, low ${datum.low}, high ${datum.high}` - } - tabIndex={({ index }) => assignIndexValue(index, 43)} - /> - } - /> - -
- - {/** ERRORBAR */} -
-

ErrorBar

- - datum.error * datum.x} - errorY={(datum) => datum.error * datum.y} - dataComponent={ - - `error bar chart, x ${datum.x}, y ${datum.y}, error margin ${datum.error}` - } - tabIndex={({ index }) => assignIndexValue(index, 60)} - /> - } - /> - -
- - {/** ACCESSIBLE GROUP */} -
-

Accessible Group

- - - } - > - - - } - /> - - - -
-
- - ); - } -} diff --git a/demo/ts/components/draggable-demo.tsx b/demo/ts/components/draggable-demo.tsx deleted file mode 100644 index 89b255ccd..000000000 --- a/demo/ts/components/draggable-demo.tsx +++ /dev/null @@ -1,286 +0,0 @@ -import React from "react"; -import { VictoryChart } from "victory-chart"; -import { VictoryAxis } from "victory-axis"; -import { VictoryBar } from "victory-bar"; -import { VictoryBrushLine } from "victory-brush-line"; -import { VictoryScatter } from "victory-scatter"; -import { - DomainTuple, - VictoryClipContainer, - Point, - Selection, - VictoryTheme, -} from "victory-core"; -import { VictoryZoomContainer } from "victory-zoom-container"; -import { VictoryBrushContainer } from "victory-brush-container"; - -type BarDataType = { - name: string; - range: DomainTuple; -}; - -type PointDataType = { - name: string; - date: Date; -}; - -type ZoomDomainType = { x?: DomainTuple; y?: DomainTuple }; - -interface DraggableDemoInterface { - bars: BarDataType[]; - points: PointDataType[]; - zoomDomain: ZoomDomainType | undefined; -} - -interface TargetPropsInterface { - scale?: number; - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - onPointChange?: Function; - datum?: {}; -} - -const bars: BarDataType[] = [ - { name: "SEA", range: [new Date(2013, 1, 1), new Date(2019, 1, 1)] }, - { name: "HKG", range: [new Date(2015, 1, 1), new Date(2015, 5, 1)] }, - { name: "LHR", range: [new Date(2016, 5, 1), new Date(2019, 1, 1)] }, - { name: "DEN", range: [new Date(2018, 8, 1), new Date(2019, 1, 1)] }, -]; - -const points: PointDataType[] = [ - { name: "SEA", date: new Date(2012, 9, 1) }, - { name: "HKG", date: new Date(2014, 3, 1) }, - { name: "LHR", date: new Date(2015, 6, 1) }, - { name: "DEN", date: new Date(2018, 3, 1) }, -]; - -class DraggablePoint extends React.Component { - static defaultEvents = [ - { - target: "data", - eventHandlers: { - onMouseOver: (evt: any, targetProps: TargetPropsInterface) => { - return [ - { - mutation: () => Object.assign(targetProps, { active: true }), - }, - ]; - }, - onMouseDown: (evt: any, targetProps: TargetPropsInterface) => { - return [ - { - mutation: () => Object.assign(targetProps, { dragging: true }), - }, - ]; - }, - onMouseMove: (evt: React.SyntheticEvent, targetProps: any) => { - const { onPointChange, datum, scale } = targetProps; - - if (targetProps.dragging) { - const { x } = Selection.getSVGEventCoordinates(evt); - const point = scale.y.invert(x); - const name = datum.name; - - onPointChange({ name, date: point }); - - return [ - { - mutation: () => Object.assign(targetProps, { x }), - }, - ]; - } - return null; - }, - onMouseUp: (evt: any, targetProps: any) => { - return [ - { - mutation: () => - Object.assign(targetProps, { dragging: false, active: false }), - }, - ]; - }, - onMouseLeave: (evt: any, targetProps: any) => { - return [ - { - mutation: () => - Object.assign(targetProps, { dragging: false, active: false }), - }, - ]; - }, - }, - }, - ]; - - render() { - return ; - } -} - -class App extends React.Component { - constructor(props: any) { - super(props); - this.state = { bars, points, zoomDomain: undefined }; - } - - handleZoom(domain: ZoomDomainType) { - this.setState({ zoomDomain: domain }); - } - - onDomainChange(domain: DomainTuple, props: any) { - const { name } = props; - const newBars = this.state.bars.map((bar) => - bar.name === name ? { name, range: domain } : bar, - ); - - this.setState({ bars: newBars }); - } - - onPointChange(point: PointDataType) { - const newPoints = this.state.points.map((p) => - p.name === point.name ? point : p, - ); - this.setState({ points: newPoints }); - } - - render() { - const containerStyle: React.CSSProperties = { - display: "flex", - flexDirection: "row", - flexWrap: "wrap", - alignItems: "center", - justifyContent: "center", - }; - - const domain: ZoomDomainType = { - y: [new Date(2012, 1, 1), new Date(2019, 1, 1)], - x: [0.5, 4.5], - }; - - const sharedProps = { - width: 800, - domain, - }; - - return ( -
- - } - /> - } - > - - - {bars.map((bar, index) => ( - (active ? 1 : 0.5), - }} - /> - } - style={{ - axis: { stroke: "none" }, - }} - axisValue={bar.name} - tickFormat={() => ""} - /> - ))} - - } - style={{ - data: { - fill: VictoryTheme.clean.palette?.colors?.cyan, - opacity: ({ active }) => (active ? 1 : 0.5), - cursor: "move", - }, - }} - x="name" - y="date" - size={10} - /> - - - } - > - - t.getFullYear()} - /> - - d.range[0]} - y0={(d) => d.range[1]} - /> - -
- ); - } -} - -export default App; diff --git a/demo/ts/components/stacked-theme-demo.tsx b/demo/ts/components/stacked-theme-demo.tsx deleted file mode 100644 index d0a4fb47c..000000000 --- a/demo/ts/components/stacked-theme-demo.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import React from "react"; -import { VictoryArea } from "victory-area"; -import { VictoryBar } from "victory-bar"; -import { VictoryChart } from "victory-chart"; -import { VictoryStack } from "victory-stack"; -import { - ColorScalePropType, - VictoryTheme, - VictoryThemeDefinition, -} from "victory-core"; -import { VictoryAxis } from "victory-axis"; - -const data = [ - { - x: 1, - y: 2, - }, - { - x: 2, - y: 3, - }, - { - x: 3, - y: 5, - }, - { - x: 4, - y: 4, - }, - { - x: 5, - y: 7, - }, -]; - -const StackedChart = ({ - theme = VictoryTheme.clean, - colorScale, - chartType = "area", - domainPadding, -}: { - theme?: VictoryThemeDefinition; - colorScale?: ColorScalePropType; - chartType?: "area" | "bar"; - domainPadding?: number; -}) => { - const chartStyle: { [key: string]: React.CSSProperties } = { - parent: { - border: "1px solid #ccc", - width: "100%", - height: 400, - display: "flex", - justifyContent: "center", - alignItems: "center", - }, - }; - const ChartComponent = chartType === "area" ? VictoryArea : VictoryBar; - return ( - - - - - - - - - - - - ); -}; - -const colorScales: ColorScalePropType[] = [ - "qualitative", - "heatmap", - "warm", - "cool", - "red", - "green", -]; - -class StackedThemeDemos extends React.Component { - render() { - const containerStyle: React.CSSProperties = { - display: "flex", - flexDirection: "row", - alignItems: "center", - justifyContent: "center", - gap: "20px", - }; - - const wrapperStyle: React.CSSProperties = { - ...containerStyle, - flexDirection: "column", - }; - - return ( -
-
-

Clean Theme

- {colorScales.map((colorScale, i) => ( - - ))} - {colorScales.map((colorScale, i) => ( - - ))} -
-
-

Material Theme

- {colorScales.map((colorScale, i) => ( - - ))} - {colorScales.map((colorScale, i) => ( - - ))} -
-
- ); - } -} - -export default StackedThemeDemos; diff --git a/demo/ts/components/victory-brush-line-demo.tsx b/demo/ts/components/victory-brush-line-demo.tsx deleted file mode 100644 index 5504a90c6..000000000 --- a/demo/ts/components/victory-brush-line-demo.tsx +++ /dev/null @@ -1,308 +0,0 @@ -import React from "react"; -import { VictoryChart } from "victory-chart"; -import { VictoryAxis } from "victory-axis"; -import { VictoryBar } from "victory-bar"; -import { VictoryBrushLine } from "victory-brush-line"; -import { VictoryLine } from "victory-line"; -import { VictoryScatter } from "victory-scatter"; -import { VictoryLabel, VictoryTheme } from "victory-core"; -import { DomainPropType, EventCallbackInterface } from "victory-core"; -import _ from "lodash"; - -type DataType = { - name: string; - strength: number; - intelligence: number; - speed: number; - luck: number; -}[]; - -interface DataSet { - name?: string; - data?: { x: string; y: number }[]; -} - -const data: DataType = [ - { name: "Adrien", strength: 5, intelligence: 30, speed: 500, luck: 3 }, - { name: "Brice", strength: 1, intelligence: 13, speed: 550, luck: 2 }, - { name: "Casey", strength: 4, intelligence: 15, speed: 80, luck: 1 }, - { name: "Drew", strength: 3, intelligence: 25, speed: 600, luck: 5 }, - { name: "Erin", strength: 9, intelligence: 50, speed: 350, luck: 4 }, - { name: "Francis", strength: 2, intelligence: 40, speed: 200, luck: 2 }, -]; - -const themeColors = VictoryTheme.clean.palette?.colors || {}; - -interface BrushLineDemoState { - maximumValues: number[]; - datasets: DataSet[]; - filters: {} | { key: any }; - activeDatasets: string[] | DataSet[]; - isFiltered: boolean; - externalMutation: EventCallbackInterface[] | undefined; -} - -const attributes: [string, string, string, string] = [ - "strength", - "intelligence", - "speed", - "luck", -]; -const height = 500; -const width = 500; -const padding: { [key: string]: number } = { - top: 100, - left: 50, - right: 50, - bottom: 50, -}; - -class App extends React.Component { - constructor(props: any) { - super(props); - - this.state = { - maximumValues: this.getMaximumValues(), - datasets: this.normalizeData(this.getMaximumValues()), - filters: {}, - activeDatasets: [], - isFiltered: false, - externalMutation: undefined, - }; - } - - getMaximumValues() { - return attributes.map((attribute: string) => { - return data.reduce((memo, datum) => { - return datum[attribute] > memo ? datum[attribute] : memo; - }, -Infinity); - }); - } - - normalizeData(maximumValues: number[]) { - // construct normalized datasets by dividing the value for each attribute by the maximum value - return data.map((datum) => ({ - name: datum.name, - data: attributes.map((attribute, i) => ({ - x: attribute, - y: datum[attribute] / maximumValues[i], - })), - })); - } - - addNewFilters(domain: DomainPropType, props: any) { - const filters = this.state.filters || {}; - const extent = domain && Math.abs(domain[1] - domain[0]); - const minVal = 1 / Number.MAX_VALUE; - filters[props.name] = extent <= minVal ? undefined : domain; - - return filters; - } - - getActiveDatasets(filters: {}): string[] { - // Return the names from all datasets that have values within all filters - const isActive = (dataset: DataSet): (string | null | undefined)[] => { - return _.keys(filters).reduce((memo: any, name) => { - if (!memo || !Array.isArray(filters[name])) { - return memo; - } - const point = _.find(dataset.data, (d) => d.x === name); - return ( - point && - Math.max(...filters[name]) >= point.y && - Math.min(...filters[name]) <= point.y - ); - }, true); - }; - - return this.state.datasets - .map((dataset) => (isActive(dataset) ? dataset.name : null) || "") - .filter(Boolean); - } - - onDomainChange(domain: DomainPropType, props: any) { - const filters = this.addNewFilters(domain, props); - const isFiltered = !_.isEmpty(_.values(filters).filter(Boolean)); - const activeDatasets = isFiltered - ? this.getActiveDatasets(filters) - : this.state.datasets; - this.setState({ activeDatasets, filters, isFiltered }); - } - - isActive(dataset: any) { - // Determine whether a given dataset is active - return !this.state.isFiltered - ? true - : _.includes(this.state.activeDatasets, dataset.name); - } - - getAxisOffset(index: number) { - const step = - (width - padding.left - padding.right) / (attributes.length - 1); - return step * index + padding.left; - } - - removeMutation() { - this.setState({ - externalMutation: undefined, - }); - } - - clearMutation() { - const callback = this.removeMutation.bind(this); - - this.setState({ - filters: {}, - activeDatasets: [], - isFiltered: false, - externalMutation: [ - { - childName: attributes, - target: "axis", - eventKey: "all", - mutation: () => { - return { brushDomain: [0, 1 / Number.MAX_VALUE] }; - }, - callback, - }, - ], - }); - } - - render() { - const containerStyle: React.CSSProperties = { - display: "flex", - flexDirection: "row", - flexWrap: "wrap", - alignItems: "center", - justifyContent: "center", - }; - - const max = this.state.maximumValues || []; - - const chartStyle: { [key: string]: React.CSSProperties } = { - parent: { border: "1px solid #ccc", margin: "2%", maxWidth: "40%" }, - }; - - return ( -
-

VictoryBrushLine

-
- - - } - /> - {this.state.datasets.map((dataset: DataSet) => ( - } - /> - ))} - {attributes.map((attribute, index) => ( - - } - offsetX={this.getAxisOffset(index)} - tickValues={[0.2, 0.4, 0.6, 0.8, 1]} - tickFormat={(tick) => Math.round(tick * max[index])} - /> - ))} - - - - - - } - externalEventMutations={this.state.externalMutation} - /> - - - - - - } - /> - - - - } /> - - - - } - /> - - - - } - /> -
-
- ); - } -} - -export default App; diff --git a/demo/ts/demo-data.ts b/demo/ts/demo-data.ts deleted file mode 100644 index 07f70593a..000000000 --- a/demo/ts/demo-data.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* ACCESSIBILITY DEMO */ - -export const accessibilityBarData = [ - { x: "A", y: 1 }, - { x: "B", y: 3 }, - { x: "C", y: 5 }, - { x: "D", y: 7 }, -]; - -export const accessibilityBoxData = [ - { x: 1, y: [5, 10, 9, 2] }, - { x: 2, y: [1, 15, 6, 8] }, - { x: 3, y: [3, 5, 6, 9] }, - { x: 4, y: [5, 20, 8, 12] }, - { x: 5, y: [2, 11, 12, 13] }, -]; - -export const accessibilityAreaData = { - a: [ - { x: 1, y: 2 }, - { x: 2, y: 3 }, - { x: 3, y: 5 }, - { x: 4, y: 4 }, - { x: 5, y: 7 }, - ], - b: [ - { x: 1, y: 1 }, - { x: 2, y: 4 }, - { x: 3, y: 5 }, - { x: 4, y: 7 }, - { x: 5, y: 5 }, - ], - c: [ - { x: 1, y: 3 }, - { x: 2, y: 2 }, - { x: 3, y: 6 }, - { x: 4, y: 2 }, - { x: 5, y: 6 }, - ], - d: [ - { x: 1, y: 2 }, - { x: 2, y: 3 }, - { x: 3, y: 3 }, - { x: 4, y: 4 }, - { x: 5, y: 7 }, - ], -}; - -export const accessibilityPieData = [ - { x: "a", y: 1, radius: 40 }, - { x: "b", y: 3, radius: 50 }, - { x: "c", y: 5, radius: 70 }, - { x: "d", y: 2, radius: 80 }, - { x: "e", y: 3, radius: 60 }, -]; - -export const accessibilityScatterData = [ - { x: 1, y: 2 }, - { x: 2, y: 3 }, - { x: 3, y: 5 }, - { x: 4, y: 4 }, - { x: 5, y: 7 }, -]; - -export const accessibilityLineData = [ - { x: 1, y: 2 }, - { x: 2, y: 3 }, - { x: 3, y: 5 }, - { x: 4, y: 4 }, - { x: 5, y: 6 }, -]; - -export const accessibilityVoronoiData = [ - { x: 11, y: 20 }, - { x: 21, y: 30 }, - { x: 31, y: 50 }, - { x: 42, y: 40 }, - { x: 52, y: 70 }, -]; - -export const accessibilityCandlestickData = [ - { x: "6/1", open: 20, close: 43, high: 66, low: 7 }, - { x: "6/2", open: 80, close: 40, high: 120, low: 10 }, - { x: "6/3", open: 50, close: 80, high: 90, low: 20 }, - { x: "6/4", open: 70, close: 22, high: 70, low: 5 }, -]; - -export const accessibilityErrorBarData = [ - { x: 10, y: 30, error: 0.2 }, - { x: 20, y: 43, error: 0.3 }, - { x: 34, y: 65, error: 0.15 }, - { x: 43, y: 50, error: 0.2 }, -]; - -export const accessibilityGroupData = { - a: [ - { x: 1, y: 1 }, - { x: 2, y: 2 }, - { x: 3, y: 5 }, - ], - b: [ - { x: 1, y: 2 }, - { x: 2, y: 1 }, - { x: 3, y: 7 }, - ], - c: [ - { x: 1, y: 3 }, - { x: 2, y: 4 }, - { x: 3, y: 9 }, - ], -}; diff --git a/demo/ts/theme/victory-axis-differential-styling-theme.ts b/demo/ts/theme/victory-axis-differential-styling-theme.ts deleted file mode 100644 index d3f6601e4..000000000 --- a/demo/ts/theme/victory-axis-differential-styling-theme.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { VictoryThemeDefinition } from "victory-core"; - -const theme: VictoryThemeDefinition = { - axis: { - style: { - axis: { - fill: "transparent", - strokeWidth: 3, - stroke: "#90A4AE", - }, - grid: { - fill: "transparent", - stroke: "#ECEFF1", - strokeWidth: 3, - }, - ticks: { - size: 15, - }, - tickLabels: { - fill: "#00796B", - }, - }, - width: 500, - height: 400, - padding: 25, - }, - independentAxis: { - style: { - axis: { - stroke: "#F4511E", - }, - ticks: { - strokeWidth: 3, - stroke: "#F4511E", - }, - }, - offsetY: 200, - }, - dependentAxis: { - style: { - axis: { - strokeWidth: 1, - }, - grid: { - strokeWidth: 1, - }, - tickLabels: { - fill: "#000000", - }, - }, - offsetX: 250, - }, -}; - -export default theme; diff --git a/website/docs/examples/axis-parallel-brush.mdx b/website/docs/examples/axis-parallel-brush.mdx index e1cd9ac41..0e2f59b28 100644 --- a/website/docs/examples/axis-parallel-brush.mdx +++ b/website/docs/examples/axis-parallel-brush.mdx @@ -4,128 +4,286 @@ title: Axis - Parallel Brush ```jsx live noInline const data = [ - { name: "Adrien", strength: 5, intelligence: 30, speed: 500, luck: 3 }, - { name: "Brice", strength: 1, intelligence: 13, speed: 550, luck: 2 }, - { name: "Casey", strength: 4, intelligence: 15, speed: 80, luck: 1 }, - { name: "Drew", strength: 3, intelligence: 25, speed: 600, luck: 5 }, - { name: "Erin", strength: 9, intelligence: 50, speed: 350, luck: 4 }, - { name: "Francis", strength: 2, intelligence: 40, speed: 200, luck: 2 } + { + name: "Adrien", + strength: 5, + intelligence: 30, + speed: 500, + luck: 3, + }, + { + name: "Brice", + strength: 1, + intelligence: 13, + speed: 550, + luck: 2, + }, + { + name: "Casey", + strength: 4, + intelligence: 15, + speed: 80, + luck: 1, + }, + { + name: "Drew", + strength: 3, + intelligence: 25, + speed: 600, + luck: 5, + }, + { + name: "Erin", + strength: 9, + intelligence: 50, + speed: 350, + luck: 4, + }, + { + name: "Francis", + strength: 2, + intelligence: 40, + speed: 200, + luck: 2, + }, +]; +const attributes = [ + "strength", + "intelligence", + "speed", + "luck", ]; -const attributes = ["strength", "intelligence", "speed", "luck"]; const height = 500; const width = 700; -const padding = { top: 100, left: 50, right: 50, bottom: 50 }; +const padding = { + top: 100, + left: 50, + right: 50, + bottom: 50, +}; function getMaximumValues() { // Find the maximum value for each axis. This will be used to normalize data and re-scale axis ticks return attributes.map((attribute) => { - return data.reduce((memo, datum) => { - return datum[attribute] > memo ? datum[attribute] : memo; - }, -Infinity); + return data.reduce( + (memo, datum) => { + return datum[attribute] > memo + ? datum[attribute] + : memo; + }, + -Infinity, + ); }); } function normalizeData(maximumValues) { // construct normalized datasets by dividing the value for each attribute by the maximum value - return data.map((datum) => ({ + return data?.map((datum) => ({ name: datum.name, - data: attributes.map((attribute, i) => ( - { x: attribute, y: datum[attribute] / maximumValues[i] } - )) + data: attributes.map( + (attribute, i) => ({ + x: attribute, + y: + datum[attribute] / + maximumValues[i], + }), + ), })); } function App() { - const maximumValues = getMaximumValues(); - const datasets = normalizeData(maximumValues); + const maximumValues = + getMaximumValues(); + const datasets = normalizeData( + maximumValues, + ); - const [state, setState] = React.useState({ - maximumValues, datasets, filters: {}, activeDatasets: [], isFiltered: false - }); + const [state, setState] = + React.useState({ + maximumValues, + datasets, + filters: {}, + activeDatasets: [], + isFiltered: false, + }); - function addNewFilters(domain, props) { + function addNewFilters( + domain, + props, + ) { const filters = state.filters || {}; - const extent = domain && Math.abs(domain[1] - domain[0]); - const minVal = 1 / Number.MAX_SAFE_INTEGER; - filters[props.name] = extent <= minVal ? undefined : domain; + const extent = + domain && + Math.abs(domain[1] - domain[0]); + const minVal = + 1 / Number.MAX_SAFE_INTEGER; + filters[props.name] = + extent <= minVal + ? undefined + : domain; return filters; } function getActiveDatasets(filters) { // Return the names from all datasets that have values within all filters const isActive = (dataset) => { - return _.keys(filters).reduce((memo, name) => { - if (!memo || !Array.isArray(filters[name])) { - return memo; - } - const point = _.find(dataset.data, (d) => d.x === name); - return point && - Math.max(...filters[name]) >= point.y && Math.min(...filters[name]) <= point.y; - }, true); + return _.keys(filters).reduce( + (memo, name) => { + if ( + !memo || + !Array.isArray( + filters[name], + ) + ) { + return memo; + } + const point = _.find( + dataset.data, + (d) => d.x === name, + ); + return ( + point && + Math.max( + ...filters[name], + ) >= point.y && + Math.min( + ...filters[name], + ) <= point.y + ); + }, + true, + ); }; - return state.datasets.map((dataset) => { - return isActive(dataset, filters) ? dataset.name : null; - }).filter(Boolean); + return state.datasets + .map((dataset) => { + return isActive( + dataset, + filters, + ) + ? dataset.name + : null; + }) + .filter(Boolean); } - function onDomainChange(domain, props) { - const filters = addNewFilters(domain, props); - const isFiltered = !_.isEmpty(_.values(filters).filter(Boolean)); - const activeDatasets = isFiltered ? getActiveDatasets(filters) : state.datasets; - setState({ activeDatasets, filters, isFiltered }); + function onDomainChange( + domain, + props, + ) { + const filters = addNewFilters( + domain, + props, + ); + const isFiltered = !_.isEmpty( + _.values(filters).filter(Boolean), + ); + const activeDatasets = isFiltered + ? getActiveDatasets(filters) + : state.datasets; + + setState((state) => ({ + ...state, + activeDatasets, + filters, + isFiltered, + })); } function isActive(dataset) { // Determine whether a given dataset is active - return !state.isFiltered ? true : _.includes(state.activeDatasets, dataset.name); + return !state.isFiltered + ? true + : _.includes( + state.activeDatasets, + dataset.name, + ); } function getAxisOffset(index) { - const step = (width - padding.left - padding.right) / (attributes.length - 1); + const step = + (width - + padding.left - + padding.right) / + (attributes.length - 1); return step * index + padding.left; } return ( - } + tickLabelComponent={ + + } /> {state.datasets.map((dataset) => ( } - style={{ data: { - opacity: isActive(dataset) ? 1 : 0.2 - } }} - /> - ))} - {attributes.map((attribute, index) => ( - - } - offsetX={getAxisOffset(index)} + key={dataset.name} + name={dataset.name} + data={dataset.data} + groupComponent={} style={{ - tickLabels: { fontSize: 15, padding: 15, pointerEvents: "none" }, + data: { + opacity: isActive(dataset) + ? 1 + : 0.2, + }, }} - tickValues={[0.2, 0.4, 0.6, 0.8, 1]} - tickFormat={(tick) => Math.round(tick * state.maximumValues[index])} /> ))} + {attributes.map( + (attribute, index) => ( + + } + offsetX={getAxisOffset( + index, + )} + style={{ + tickLabels: { + fontSize: 15, + padding: 15, + pointerEvents: "none", + }, + }} + tickValues={[ + 0.2, 0.4, 0.6, 0.8, 1, + ]} + tickFormat={(tick) => + Math.round( + tick * + state.maximumValues[ + index + ], + ) + } + /> + ), + )} ); } -render(); +render(); ``` diff --git a/website/docs/examples/bar-linked-brushing.mdx b/website/docs/examples/bar-linked-brushing.mdx new file mode 100644 index 000000000..9d69e4690 --- /dev/null +++ b/website/docs/examples/bar-linked-brushing.mdx @@ -0,0 +1,381 @@ +--- +title: Bar - Linked Brushing +--- + +```jsx live noInline +const barData = [ + { + name: "SEA", + range: [ + new Date(2013, 1, 1), + new Date(2019, 1, 1), + ], + }, + { + name: "HKG", + range: [ + new Date(2015, 1, 1), + new Date(2015, 5, 1), + ], + }, + { + name: "LHR", + range: [ + new Date(2016, 5, 1), + new Date(2019, 1, 1), + ], + }, + { + name: "DEN", + range: [ + new Date(2018, 8, 1), + new Date(2019, 1, 1), + ], + }, +]; + +const pointData = [ + { + name: "SEA", + date: new Date(2012, 9, 1), + }, + { + name: "HKG", + date: new Date(2014, 3, 1), + }, + { + name: "LHR", + date: new Date(2015, 6, 1), + }, + { + name: "DEN", + date: new Date(2018, 3, 1), + }, +]; + +const containerStyle = { + display: "flex", + flexDirection: "row", + flexWrap: "wrap", + alignItems: "center", + justifyContent: "center", + paddingBottom: 50 +}; + +const domain = { + y: [ + new Date(2012, 1, 1), + new Date(2019, 1, 1), + ], + x: [0.5, 4.5], +}; + +const sharedProps = { + width: 800, + domain, +}; + +class DraggablePoint extends React.Component { + static defaultEvents = [ + { + target: "data", + eventHandlers: { + onMouseOver: ( + evt, + targetProps, + ) => { + return [ + { + mutation: () => + Object.assign( + targetProps, + { active: true }, + ), + }, + ]; + }, + onMouseDown: ( + evt, + targetProps, + ) => { + return [ + { + mutation: () => + Object.assign( + targetProps, + { dragging: true }, + ), + }, + ]; + }, + onMouseMove: ( + evt, + targetProps, + ) => { + const { + onPointChange, + datum, + scale, + } = targetProps; + + if (targetProps.dragging) { + const { x } = + Selection.getSVGEventCoordinates( + evt, + ); + const point = + scale.y.invert(x); + const name = datum.name; + + onPointChange({ + name, + date: point, + }); + + return [ + { + mutation: () => + Object.assign( + targetProps, + { x }, + ), + }, + ]; + } + return null; + }, + onMouseUp: ( + evt, + targetProps, + ) => { + return [ + { + mutation: () => + Object.assign( + targetProps, + { + dragging: false, + active: false, + }, + ), + }, + ]; + }, + onMouseLeave: ( + evt, + targetProps, + ) => { + return [ + { + mutation: () => + Object.assign( + targetProps, + { + dragging: false, + active: false, + }, + ), + }, + ]; + }, + }, + }, + ]; + + render() { + return ; + } +} + +function App() { + const [zoomDomain, setZoomDomain] = + React.useState({}); + const [bars, setBars] = + React.useState(barData); + const [points, setPoints] = + React.useState(pointData); + + function handleZoom(domain) { + setZoomDomain(domain); + } + + function onDomainChange( + domain, + props, + ) { + const { name } = props; + const newBars = bars.map((bar) => + bar.name === name + ? { name, range: domain } + : bar, + ); + + setBars(newBars); + } + + function onPointChange(point) { + const newPoints = points.map((p) => + p.name === point.name ? point : p, + ); + setPoints(newPoints); + } + + return ( +
+ + } + /> + } + > + + + {bars.map((bar, index) => ( + + active ? 1 : 0.5, + }} + /> + } + style={{ + axis: { stroke: "none" }, + }} + axisValue={bar.name} + tickFormat={() => ""} + /> + ))} + + } + style={{ + data: { + fill: VictoryTheme.clean + .palette?.colors?.cyan, + opacity: ({ active }) => + active ? 1 : 0.5, + cursor: "move", + }, + }} + x="name" + y="date" + size={10} + /> + + + } + > + + + t.getFullYear() + } + /> + + d.range[0]} + y0={(d) => d.range[1]} + /> + +
+ ); +} + +render(); +``` diff --git a/website/docs/examples/brush-zoom.mdx b/website/docs/examples/brush-zoom.mdx deleted file mode 100644 index 427b19cfb..000000000 --- a/website/docs/examples/brush-zoom.mdx +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: Brush & Zoom - Linked Charts ---- - -```jsx live noInline -function App() { - const [state, setState] = React.useState({ - zoomDomain: { x: [new Date(1990, 1, 1), new Date(2009, 1, 1)] } - }); - - function handleZoom(domain) { - setState({ zoomDomain: domain }); - } - - return ( -
- - } - theme={VictoryTheme.clean} - > - - - - - } - theme={VictoryTheme.clean} - > - new Date(x).getFullYear()} - /> - - -
- ); -} - -render(); -``` diff --git a/website/docs/guides/accessibility.mdx b/website/docs/guides/accessibility.mdx new file mode 100644 index 000000000..29c011d77 --- /dev/null +++ b/website/docs/guides/accessibility.mdx @@ -0,0 +1,100 @@ +--- +title: Accessibility +--- + +Victory provides a number of features to make your charts more accessible. This guide will walk you through some of the most important features. + +## Basic + +Containers like `VictoryChart` set the `role` attribute to `img` and expose the `desc` prop to provide a description of the chart for screen readers. + +Chart types like `VictoryLine` and `VictoryBar` expose aria props like `aria-label` to provide additional context for screen readers. Adding a `tabIndex` attribute will make the data components focusable. (click on chart and press TAB key to focus) + +```jsx live + + + `x: ${datum.x}` + } + tabIndex={0} + /> + } + /> + +``` + +## Groups + +Use `VictoryAccessibleGroup` to wrap a group of chart components. This will add a `role="group"` attribute to the SVG element, which will make the chart more accessible to screen readers. Adding a `tabIndex` attribute will make the group focusable. (click on chart and press TAB key to focus) + +```jsx live + + + } + > + + `x: ${datum.x}` + } + tabIndex={0} + /> + } + /> + + `x: ${datum.x}` + } + tabIndex={0} + /> + } + /> + + +``` diff --git a/website/docs/guides/axis.mdx b/website/docs/guides/axis.mdx index cae60fe88..5a34a0823 100644 --- a/website/docs/guides/axis.mdx +++ b/website/docs/guides/axis.mdx @@ -82,7 +82,10 @@ Gridlines can be shown by styling the axis component. dependentAxis style={{ grid: { - stroke: ({ tick }) => (tick === 5 ? "#2d7ff9" : "#CFD8DC"), + stroke: ({ tick }) => + tick === 5 + ? "#2d7ff9" + : "#CFD8DC", strokeDasharray: "10, 5", }, }} @@ -606,6 +609,37 @@ Fixing axis label and tick label overlap using the style prop. --- +### Axis - Brush Lines + +Brush lines can be added to the axis using the `VictoryBrushLine` component for selecting a range of the domain. + +:::info +See the full API for [`VictoryBrushLine`](/docs/api/victory-brush-line) for more details. +::: + +```jsx live + + + + } + /> + +``` + +--- + ## VictoryPolarAxis Creates a circular axis for a chart.