diff --git a/apps/demos/Demos/Charts/Stock/Angular/app/app.component.html b/apps/demos/Demos/Charts/Stock/Angular/app/app.component.html index b2564bc3cc80..5d981845141f 100644 --- a/apps/demos/Demos/Charts/Stock/Angular/app/app.component.html +++ b/apps/demos/Demos/Charts/Stock/Angular/app/app.component.html @@ -1,3 +1,35 @@ + + + + + + + + + + + + - + + + + + - - + + diff --git a/apps/demos/Demos/Charts/Stock/Angular/app/app.component.ts b/apps/demos/Demos/Charts/Stock/Angular/app/app.component.ts index 712c237092d3..0ee16d02095f 100644 --- a/apps/demos/Demos/Charts/Stock/Angular/app/app.component.ts +++ b/apps/demos/Demos/Charts/Stock/Angular/app/app.component.ts @@ -1,12 +1,27 @@ import { bootstrapApplication } from '@angular/platform-browser'; -import { Component, enableProdMode, provideZoneChangeDetection } from '@angular/core'; -import { DxChartModule } from 'devextreme-angular'; +import { + ChangeDetectorRef, Component, enableProdMode, provideZoneChangeDetection, +} from '@angular/core'; +import { DxChartModule, DxToolbarModule } from 'devextreme-angular'; +import type { DxChartTypes } from 'devextreme-angular/ui/chart'; +import type { DxButtonTypes } from 'devextreme-angular/ui/button'; +import type { VisualRange } from 'devextreme-angular/common/charts'; import { StockPrice, Service } from './app.service'; if (!/localhost/.test(document.location.host)) { enableProdMode(); } +const dayMs = 24 * 60 * 60 * 1000; +const panStepFraction = 0.25; +const zoomFactor = 0.75; +const minVisualRangeLength = { days: 3 }; +const minVisibleSpanMs = minVisualRangeLength.days * dayMs; + +function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); +} + @Component({ selector: 'demo-app', templateUrl: './app.component.html', @@ -14,21 +29,121 @@ if (!/localhost/.test(document.location.host)) { providers: [Service], imports: [ DxChartModule, + DxToolbarModule, ], }) export class AppComponent { + minVisualRange = minVisualRangeLength; + stockPrices: StockPrice[]; - constructor(service: Service) { + initialStart: Date; + + initialEnd = new Date(1994, 3, 29, 12); + + visualRange: VisualRange; + + dataMin: number; + + dataMax: number; + + rangeMin: number; + + rangeMax: number; + + panLeftButtonOptions: DxButtonTypes.Properties = { + icon: 'chevronleft', + hint: 'Pan Left', + elementAttr: { 'aria-label': 'Pan Left' }, + onClick: () => this.pan(-1), + }; + + panRightButtonOptions: DxButtonTypes.Properties = { + icon: 'chevronright', + hint: 'Pan Right', + elementAttr: { 'aria-label': 'Pan Right' }, + onClick: () => this.pan(1), + }; + + zoomInButtonOptions: DxButtonTypes.Properties = { + icon: 'plus', + hint: 'Zoom In', + elementAttr: { 'aria-label': 'Zoom In' }, + onClick: () => this.zoom(zoomFactor), + }; + + zoomOutButtonOptions: DxButtonTypes.Properties = { + icon: 'minus', + hint: 'Zoom Out', + elementAttr: { 'aria-label': 'Zoom Out' }, + onClick: () => this.zoom(1 / zoomFactor), + }; + + resetZoomButtonOptions: DxButtonTypes.Properties = { + icon: 'refresh', + hint: 'Reset Zoom', + elementAttr: { 'aria-label': 'Reset Zoom' }, + onClick: () => this.resetZoom(), + }; + + constructor(service: Service, private changeDetectorRef: ChangeDetectorRef) { this.stockPrices = service.getStockPrices(); + this.dataMin = this.stockPrices[0].date.getTime(); + this.dataMax = this.stockPrices[this.stockPrices.length - 1].date.getTime(); + this.rangeMin = this.dataMin - dayMs / 2; + this.rangeMax = this.dataMax + dayMs / 2; + this.initialStart = new Date(this.rangeMin); + this.visualRange = { + startValue: this.initialStart, + endValue: this.initialEnd, + }; + } + + getRange() { + return { + start: (this.visualRange.startValue as Date).getTime(), + end: (this.visualRange.endValue as Date).getTime(), + }; + } + + setRange(start: number, end: number) { + const span = clamp(end - start, minVisibleSpanMs, this.rangeMax - this.rangeMin); + const center = (start + end) / 2; + const newStart = clamp(center - span / 2, this.rangeMin, this.rangeMax - span); + + this.visualRange = { + startValue: new Date(newStart), + endValue: new Date(newStart + span), + }; + this.changeDetectorRef.detectChanges(); + } + + pan(direction: number) { + const { start, end } = this.getRange(); + const shift = (end - start) * panStepFraction * direction; + this.setRange(start + shift, end + shift); + } + + zoom(factor: number) { + const { start, end } = this.getRange(); + const center = (start + end) / 2; + const half = ((end - start) * factor) / 2; + this.setRange(center - half, center + half); + } + + resetZoom() { + this.setRange(this.initialStart.getTime(), this.initialEnd.getTime()); } - customizeTooltip(arg) { + customizeTooltip(arg: DxChartTypes.PointInfo) { + if (!('openValue' in arg)) { + return { text: '' }; + } return { - text: `Open: $${arg.openValue}
` - + `Close: $${arg.closeValue}
` - + `High: $${arg.highValue}
` - + `Low: $${arg.lowValue}
`, + text: `Open: ${arg.openValueText}
` + + `Close: ${arg.closeValueText}
` + + `High: ${arg.highValueText}
` + + `Low: ${arg.lowValueText}
`, }; } } diff --git a/apps/demos/Demos/Charts/Stock/Angular/app/app.service.ts b/apps/demos/Demos/Charts/Stock/Angular/app/app.service.ts index 0240c22abc90..31902d64df8d 100644 --- a/apps/demos/Demos/Charts/Stock/Angular/app/app.service.ts +++ b/apps/demos/Demos/Charts/Stock/Angular/app/app.service.ts @@ -12,145 +12,46 @@ export class StockPrice { c: number; } -const stockPrices: StockPrice[] = [{ - date: new Date(1994, 2, 1), - l: 24.00, - h: 25.00, - o: 25.00, - c: 24.875, -}, { - date: new Date(1994, 2, 2), - l: 23.625, - h: 25.125, - o: 24.00, - c: 24.875, -}, { - date: new Date(1994, 2, 3), - l: 26.25, - h: 28.25, - o: 26.75, - c: 27.00, -}, { - date: new Date(1994, 2, 4), - l: 26.50, - h: 27.875, - o: 26.875, - c: 27.25, -}, { - date: new Date(1994, 2, 7), - l: 26.375, - h: 27.50, - o: 27.375, - c: 26.75, -}, { - date: new Date(1994, 2, 8), - l: 25.75, - h: 26.875, - o: 26.75, - c: 26.00, -}, { - date: new Date(1994, 2, 9), - l: 25.75, - h: 26.75, - o: 26.125, - c: 26.25, -}, { - date: new Date(1994, 2, 10), - l: 25.75, - h: 26.375, - o: 26.375, - c: 25.875, -}, { - date: new Date(1994, 2, 11), - l: 24.875, - h: 26.125, - o: 26.00, - c: 25.375, -}, { - date: new Date(1994, 2, 14), - l: 25.125, - h: 26.00, - o: 25.625, - c: 25.75, -}, { - date: new Date(1994, 2, 15), - l: 25.875, - h: 26.625, - o: 26.125, - c: 26.375, -}, { - date: new Date(1994, 2, 16), - l: 26.25, - h: 27.375, - o: 26.25, - c: 27.25, -}, { - date: new Date(1994, 2, 17), - l: 26.875, - h: 27.25, - o: 27.125, - c: 26.875, -}, { - date: new Date(1994, 2, 18), - l: 26.375, - h: 27.125, - o: 27.00, - c: 27.125, -}, { - date: new Date(1994, 2, 21), - l: 26.75, - h: 27.875, - o: 26.875, - c: 27.75, -}, { - date: new Date(1994, 2, 22), - l: 26.75, - h: 28.375, - o: 27.50, - c: 27.00, -}, { - date: new Date(1994, 2, 23), - l: 26.875, - h: 28.125, - o: 27.00, - c: 28.00, -}, { - date: new Date(1994, 2, 24), - l: 26.25, - h: 27.875, - o: 27.75, - c: 27.625, -}, { - date: new Date(1994, 2, 25), - l: 27.50, - h: 28.75, - o: 27.75, - c: 28.00, -}, { - date: new Date(1994, 2, 28), - l: 25.75, - h: 28.25, - o: 28.00, - c: 27.25, -}, { - date: new Date(1994, 2, 29), - l: 26.375, - h: 27.50, - o: 27.50, - c: 26.875, -}, { - date: new Date(1994, 2, 30), - l: 25.75, - h: 27.50, - o: 26.375, - c: 26.25, -}, { - date: new Date(1994, 2, 31), - l: 24.75, - h: 27.00, - o: 26.50, - c: 25.25, -}]; +function generateStockData(startDate: Date, days: number): StockPrice[] { + let seed = 12345; + + function random(): number { + seed = (seed * 16807) % 2147483647; + return (seed - 1) / 2147483646; + } + + function randomOffset(amplitude: number): number { + return (random() - 0.5) * amplitude; + } + + const data: StockPrice[] = []; + const current = new Date(startDate); + let price = 25; + + while (data.length < days) { + const day = current.getDay(); + if (day !== 0 && day !== 6) { + const open = price + randomOffset(1.5); + const close = open + randomOffset(1.5); + const high = Math.max(open, close) + random() * 0.8; + const low = Math.min(open, close) - random() * 0.8; + + data.push({ + date: new Date(current), + o: +open.toFixed(2), + h: +high.toFixed(2), + l: +low.toFixed(2), + c: +close.toFixed(2), + }); + + price = close; + } + current.setDate(current.getDate() + 1); + } + return data; +} + +const stockPrices: StockPrice[] = generateStockData(new Date(1994, 3, 1), 65); @Injectable() export class Service { diff --git a/apps/demos/Demos/Charts/Stock/React/App.tsx b/apps/demos/Demos/Charts/Stock/React/App.tsx index fa73f5c3571e..d4177c67ce32 100644 --- a/apps/demos/Demos/Charts/Stock/React/App.tsx +++ b/apps/demos/Demos/Charts/Stock/React/App.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import Chart, { CommonSeriesSettings, Series, @@ -8,54 +8,193 @@ import Chart, { Format, ValueAxis, Title, + ZoomAndPan, + ScrollBar, + Legend, Export, Tooltip, } from 'devextreme-react/chart'; import type { ChartTypes } from 'devextreme-react/chart'; +import type { VisualRange } from 'devextreme-react/common/charts'; +import type { ButtonTypes } from 'devextreme-react/button'; +import Toolbar, { Item } from 'devextreme-react/toolbar'; import { dataSource } from './data.ts'; +const dayMs = 24 * 60 * 60 * 1000; +const panStepFraction = 0.25; +const zoomFactor = 0.75; +const minVisualRangeLength = { days: 3 }; +const minVisibleSpanMs = minVisualRangeLength.days * dayMs; +const tooltipFormat = { type: 'currency', precision: 2 } as const; + +const initialEnd = new Date(1994, 3, 29, 12); + +const dataMin = dataSource[0].date.getTime(); +const dataMax = dataSource[dataSource.length - 1].date.getTime(); +const rangeMin = dataMin - dayMs / 2; +const rangeMax = dataMax + dayMs / 2; +const initialStart = new Date(rangeMin); + +function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); +} + +function getRange(range: VisualRange) { + return { + start: (range.startValue as Date).getTime(), + end: (range.endValue as Date).getTime(), + }; +} + +function getNextRange(start: number, end: number): VisualRange { + const span = clamp(end - start, minVisibleSpanMs, rangeMax - rangeMin); + const center = (start + end) / 2; + const newStart = clamp(center - span / 2, rangeMin, rangeMax - span); + + return { + startValue: new Date(newStart), + endValue: new Date(newStart + span), + }; +} + function customizeTooltip(arg: ChartTypes.PointInfo): Record { if (!('openValue' in arg)) { return { text: '' }; } return { - text: `Open: $${arg.openValue}
- Close: $${arg.closeValue}
- High: $${arg.highValue}
- Low: $${arg.lowValue}
`, + text: [ + `Open: ${arg.openValueText}`, + `Close: ${arg.closeValueText}`, + `High: ${arg.highValueText}`, + `Low: ${arg.lowValueText}`, + ].join('
'), }; } function App() { + const [visualRange, setVisualRange] = useState({ + startValue: initialStart, + endValue: initialEnd, + }); + + const onVisualRangeChange = useCallback((range: VisualRange | (Date | number | string)[]) => { + setVisualRange(range as VisualRange); + }, []); + + const pan = useCallback((direction: number) => { + setVisualRange((range) => { + const { start, end } = getRange(range); + const shift = (end - start) * panStepFraction * direction; + return getNextRange(start + shift, end + shift); + }); + }, []); + + const zoom = useCallback((factor: number) => { + setVisualRange((range) => { + const { start, end } = getRange(range); + const center = (start + end) / 2; + const half = ((end - start) * factor) / 2; + return getNextRange(center - half, center + half); + }); + }, []); + + const resetZoom = useCallback(() => { + setVisualRange(getNextRange(initialStart.getTime(), initialEnd.getTime())); + }, []); + + const panLeftButtonOptions = useMemo(() => ({ + icon: 'chevronleft', + hint: 'Pan Left', + elementAttr: { 'aria-label': 'Pan Left' }, + onClick: () => pan(-1), + }), [pan]); + + const panRightButtonOptions = useMemo(() => ({ + icon: 'chevronright', + hint: 'Pan Right', + elementAttr: { 'aria-label': 'Pan Right' }, + onClick: () => pan(1), + }), [pan]); + + const zoomInButtonOptions = useMemo(() => ({ + icon: 'plus', + hint: 'Zoom In', + elementAttr: { 'aria-label': 'Zoom In' }, + onClick: () => zoom(zoomFactor), + }), [zoom]); + + const zoomOutButtonOptions = useMemo(() => ({ + icon: 'minus', + hint: 'Zoom Out', + elementAttr: { 'aria-label': 'Zoom Out' }, + onClick: () => zoom(1 / zoomFactor), + }), [zoom]); + + const resetZoomButtonOptions = useMemo(() => ({ + icon: 'refresh', + hint: 'Reset Zoom', + elementAttr: { 'aria-label': 'Reset Zoom' }, + onClick: resetZoom, + }), [resetZoom]); + return ( - - - + + + + + + + + - - - - - - - <Label> - <Format precision={0} type="currency" /> - </Label> - </ValueAxis> - <Export enabled={true} /> - <Tooltip - enabled={true} - customizeTooltip={customizeTooltip} - location="edge" - /> - </Chart> + <CommonSeriesSettings argumentField="date" type="stock" /> + <Series + name="E-Mart" + openValueField="o" + highValueField="h" + lowValueField="l" + closeValueField="c" + > + <Reduction color="red" /> + </Series> + <ZoomAndPan + argumentAxis="both" + dragToZoom={true} + allowMouseWheel={true} + /> + <ScrollBar visible={true} position="bottom" /> + <Legend verticalAlignment="bottom" horizontalAlignment="center" /> + <ArgumentAxis + visualRange={visualRange} + onVisualRangeChange={onVisualRangeChange} + minVisualRangeLength={minVisualRangeLength} + > + <Label + format="MMM dd" + overlappingBehavior="rotate" + rotationAngle={30} + /> + </ArgumentAxis> + <ValueAxis tickInterval={1} position="right"> + <Title text="US dollars" /> + <Label> + <Format precision={0} type="currency" /> + </Label> + </ValueAxis> + <Export enabled={true} /> + <Tooltip + enabled={true} + format={tooltipFormat} + customizeTooltip={customizeTooltip} + location="edge" + /> + </Chart> + </> ); } diff --git a/apps/demos/Demos/Charts/Stock/React/data.ts b/apps/demos/Demos/Charts/Stock/React/data.ts index 29303780dd9e..1130d8f41409 100644 --- a/apps/demos/Demos/Charts/Stock/React/data.ts +++ b/apps/demos/Demos/Charts/Stock/React/data.ts @@ -1,139 +1,48 @@ -export const dataSource = [{ - date: new Date(1994, 2, 1), - l: 24.00, - h: 25.00, - o: 25.00, - c: 24.875, -}, { - date: new Date(1994, 2, 2), - l: 23.625, - h: 25.125, - o: 24.00, - c: 24.875, -}, { - date: new Date(1994, 2, 3), - l: 26.25, - h: 28.25, - o: 26.75, - c: 27.00, -}, { - date: new Date(1994, 2, 4), - l: 26.50, - h: 27.875, - o: 26.875, - c: 27.25, -}, { - date: new Date(1994, 2, 7), - l: 26.375, - h: 27.50, - o: 27.375, - c: 26.75, -}, { - date: new Date(1994, 2, 8), - l: 25.75, - h: 26.875, - o: 26.75, - c: 26.00, -}, { - date: new Date(1994, 2, 9), - l: 25.75, - h: 26.75, - o: 26.125, - c: 26.25, -}, { - date: new Date(1994, 2, 10), - l: 25.75, - h: 26.375, - o: 26.375, - c: 25.875, -}, { - date: new Date(1994, 2, 11), - l: 24.875, - h: 26.125, - o: 26.00, - c: 25.375, -}, { - date: new Date(1994, 2, 14), - l: 25.125, - h: 26.00, - o: 25.625, - c: 25.75, -}, { - date: new Date(1994, 2, 15), - l: 25.875, - h: 26.625, - o: 26.125, - c: 26.375, -}, { - date: new Date(1994, 2, 16), - l: 26.25, - h: 27.375, - o: 26.25, - c: 27.25, -}, { - date: new Date(1994, 2, 17), - l: 26.875, - h: 27.25, - o: 27.125, - c: 26.875, -}, { - date: new Date(1994, 2, 18), - l: 26.375, - h: 27.125, - o: 27.00, - c: 27.125, -}, { - date: new Date(1994, 2, 21), - l: 26.75, - h: 27.875, - o: 26.875, - c: 27.75, -}, { - date: new Date(1994, 2, 22), - l: 26.75, - h: 28.375, - o: 27.50, - c: 27.00, -}, { - date: new Date(1994, 2, 23), - l: 26.875, - h: 28.125, - o: 27.00, - c: 28.00, -}, { - date: new Date(1994, 2, 24), - l: 26.25, - h: 27.875, - o: 27.75, - c: 27.625, -}, { - date: new Date(1994, 2, 25), - l: 27.50, - h: 28.75, - o: 27.75, - c: 28.00, -}, { - date: new Date(1994, 2, 28), - l: 25.75, - h: 28.25, - o: 28.00, - c: 27.25, -}, { - date: new Date(1994, 2, 29), - l: 26.375, - h: 27.50, - o: 27.50, - c: 26.875, -}, { - date: new Date(1994, 2, 30), - l: 25.75, - h: 27.50, - o: 26.375, - c: 26.25, -}, { - date: new Date(1994, 2, 31), - l: 24.75, - h: 27.00, - o: 26.50, - c: 25.25, -}]; +export interface StockPrice { + date: Date; + l: number; + h: number; + o: number; + c: number; +} + +function generateStockData(startDate: Date, days: number): StockPrice[] { + let seed = 12345; + + function random(): number { + seed = (seed * 16807) % 2147483647; + return (seed - 1) / 2147483646; + } + + function randomOffset(amplitude: number): number { + return (random() - 0.5) * amplitude; + } + + const data: StockPrice[] = []; + const current = new Date(startDate); + let price = 25; + + while (data.length < days) { + const day = current.getDay(); + if (day !== 0 && day !== 6) { + const open = price + randomOffset(1.5); + const close = open + randomOffset(1.5); + const high = Math.max(open, close) + random() * 0.8; + const low = Math.min(open, close) - random() * 0.8; + + data.push({ + date: new Date(current), + o: +open.toFixed(2), + h: +high.toFixed(2), + l: +low.toFixed(2), + c: +close.toFixed(2), + }); + + price = close; + } + current.setDate(current.getDate() + 1); + } + return data; +} + +export const dataSource = generateStockData(new Date(1994, 3, 1), 65); diff --git a/apps/demos/Demos/Charts/Stock/ReactJs/App.js b/apps/demos/Demos/Charts/Stock/ReactJs/App.js index 24cf604fc115..4cb975ca27aa 100644 --- a/apps/demos/Demos/Charts/Stock/ReactJs/App.js +++ b/apps/demos/Demos/Charts/Stock/ReactJs/App.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import Chart, { CommonSeriesSettings, Series, @@ -8,61 +8,221 @@ import Chart, { Format, ValueAxis, Title, + ZoomAndPan, + ScrollBar, + Legend, Export, Tooltip, } from 'devextreme-react/chart'; +import Toolbar, { Item } from 'devextreme-react/toolbar'; import { dataSource } from './data.js'; +const dayMs = 24 * 60 * 60 * 1000; +const panStepFraction = 0.25; +const zoomFactor = 0.75; +const minVisualRangeLength = { days: 3 }; +const minVisibleSpanMs = minVisualRangeLength.days * dayMs; +const tooltipFormat = { type: 'currency', precision: 2 }; +const initialEnd = new Date(1994, 3, 29, 12); +const dataMin = dataSource[0].date.getTime(); +const dataMax = dataSource[dataSource.length - 1].date.getTime(); +const rangeMin = dataMin - dayMs / 2; +const rangeMax = dataMax + dayMs / 2; +const initialStart = new Date(rangeMin); +function clamp(value, min, max) { + return Math.min(Math.max(value, min), max); +} +function getRange(range) { + return { + start: range.startValue.getTime(), + end: range.endValue.getTime(), + }; +} +function getNextRange(start, end) { + const span = clamp(end - start, minVisibleSpanMs, rangeMax - rangeMin); + const center = (start + end) / 2; + const newStart = clamp(center - span / 2, rangeMin, rangeMax - span); + return { + startValue: new Date(newStart), + endValue: new Date(newStart + span), + }; +} function customizeTooltip(arg) { if (!('openValue' in arg)) { return { text: '' }; } return { - text: `Open: $${arg.openValue}<br/> - Close: $${arg.closeValue}<br/> - High: $${arg.highValue}<br/> - Low: $${arg.lowValue}<br/>`, + text: [ + `Open: ${arg.openValueText}`, + `Close: ${arg.closeValueText}`, + `High: ${arg.highValueText}`, + `Low: ${arg.lowValueText}`, + ].join('<br/>'), }; } function App() { + const [visualRange, setVisualRange] = useState({ + startValue: initialStart, + endValue: initialEnd, + }); + const onVisualRangeChange = useCallback((range) => { + setVisualRange(range); + }, []); + const pan = useCallback((direction) => { + setVisualRange((range) => { + const { start, end } = getRange(range); + const shift = (end - start) * panStepFraction * direction; + return getNextRange(start + shift, end + shift); + }); + }, []); + const zoom = useCallback((factor) => { + setVisualRange((range) => { + const { start, end } = getRange(range); + const center = (start + end) / 2; + const half = ((end - start) * factor) / 2; + return getNextRange(center - half, center + half); + }); + }, []); + const resetZoom = useCallback(() => { + setVisualRange(getNextRange(initialStart.getTime(), initialEnd.getTime())); + }, []); + const panLeftButtonOptions = useMemo( + () => ({ + icon: 'chevronleft', + hint: 'Pan Left', + elementAttr: { 'aria-label': 'Pan Left' }, + onClick: () => pan(-1), + }), + [pan], + ); + const panRightButtonOptions = useMemo( + () => ({ + icon: 'chevronright', + hint: 'Pan Right', + elementAttr: { 'aria-label': 'Pan Right' }, + onClick: () => pan(1), + }), + [pan], + ); + const zoomInButtonOptions = useMemo( + () => ({ + icon: 'plus', + hint: 'Zoom In', + elementAttr: { 'aria-label': 'Zoom In' }, + onClick: () => zoom(zoomFactor), + }), + [zoom], + ); + const zoomOutButtonOptions = useMemo( + () => ({ + icon: 'minus', + hint: 'Zoom Out', + elementAttr: { 'aria-label': 'Zoom Out' }, + onClick: () => zoom(1 / zoomFactor), + }), + [zoom], + ); + const resetZoomButtonOptions = useMemo( + () => ({ + icon: 'refresh', + hint: 'Reset Zoom', + elementAttr: { 'aria-label': 'Reset Zoom' }, + onClick: resetZoom, + }), + [resetZoom], + ); return ( - <Chart - id="chart" - title="Stock Price" - dataSource={dataSource} - > - <CommonSeriesSettings - argumentField="date" - type="stock" - /> - <Series - name="E-Mart" - openValueField="o" - highValueField="h" - lowValueField="l" - closeValueField="c" + <> + <Toolbar> + <Item + location="after" + widget="dxButton" + options={panLeftButtonOptions} + /> + <Item + location="after" + widget="dxButton" + options={panRightButtonOptions} + /> + <Item + location="after" + widget="dxButton" + options={zoomInButtonOptions} + /> + <Item + location="after" + widget="dxButton" + options={zoomOutButtonOptions} + /> + <Item + location="after" + widget="dxButton" + options={resetZoomButtonOptions} + /> + </Toolbar> + <Chart + id="chart" + title="Stock Price" + dataSource={dataSource} > - <Reduction color="red" /> - </Series> - <ArgumentAxis workdaysOnly={true}> - <Label format="shortDate" /> - </ArgumentAxis> - <ValueAxis tickInterval={1}> - <Title text="US dollars" /> - <Label> - <Format - precision={0} - type="currency" + <CommonSeriesSettings + argumentField="date" + type="stock" + /> + <Series + name="E-Mart" + openValueField="o" + highValueField="h" + lowValueField="l" + closeValueField="c" + > + <Reduction color="red" /> + </Series> + <ZoomAndPan + argumentAxis="both" + dragToZoom={true} + allowMouseWheel={true} + /> + <ScrollBar + visible={true} + position="bottom" + /> + <Legend + verticalAlignment="bottom" + horizontalAlignment="center" + /> + <ArgumentAxis + visualRange={visualRange} + onVisualRangeChange={onVisualRangeChange} + minVisualRangeLength={minVisualRangeLength} + > + <Label + format="MMM dd" + overlappingBehavior="rotate" + rotationAngle={30} /> - </Label> - </ValueAxis> - <Export enabled={true} /> - <Tooltip - enabled={true} - customizeTooltip={customizeTooltip} - location="edge" - /> - </Chart> + </ArgumentAxis> + <ValueAxis + tickInterval={1} + position="right" + > + <Title text="US dollars" /> + <Label> + <Format + precision={0} + type="currency" + /> + </Label> + </ValueAxis> + <Export enabled={true} /> + <Tooltip + enabled={true} + format={tooltipFormat} + customizeTooltip={customizeTooltip} + location="edge" + /> + </Chart> + </> ); } export default App; diff --git a/apps/demos/Demos/Charts/Stock/ReactJs/data.js b/apps/demos/Demos/Charts/Stock/ReactJs/data.js index 8ac0d34d2404..0361f3fea288 100644 --- a/apps/demos/Demos/Charts/Stock/ReactJs/data.js +++ b/apps/demos/Demos/Charts/Stock/ReactJs/data.js @@ -1,163 +1,33 @@ -export const dataSource = [ - { - date: new Date(1994, 2, 1), - l: 24.0, - h: 25.0, - o: 25.0, - c: 24.875, - }, - { - date: new Date(1994, 2, 2), - l: 23.625, - h: 25.125, - o: 24.0, - c: 24.875, - }, - { - date: new Date(1994, 2, 3), - l: 26.25, - h: 28.25, - o: 26.75, - c: 27.0, - }, - { - date: new Date(1994, 2, 4), - l: 26.5, - h: 27.875, - o: 26.875, - c: 27.25, - }, - { - date: new Date(1994, 2, 7), - l: 26.375, - h: 27.5, - o: 27.375, - c: 26.75, - }, - { - date: new Date(1994, 2, 8), - l: 25.75, - h: 26.875, - o: 26.75, - c: 26.0, - }, - { - date: new Date(1994, 2, 9), - l: 25.75, - h: 26.75, - o: 26.125, - c: 26.25, - }, - { - date: new Date(1994, 2, 10), - l: 25.75, - h: 26.375, - o: 26.375, - c: 25.875, - }, - { - date: new Date(1994, 2, 11), - l: 24.875, - h: 26.125, - o: 26.0, - c: 25.375, - }, - { - date: new Date(1994, 2, 14), - l: 25.125, - h: 26.0, - o: 25.625, - c: 25.75, - }, - { - date: new Date(1994, 2, 15), - l: 25.875, - h: 26.625, - o: 26.125, - c: 26.375, - }, - { - date: new Date(1994, 2, 16), - l: 26.25, - h: 27.375, - o: 26.25, - c: 27.25, - }, - { - date: new Date(1994, 2, 17), - l: 26.875, - h: 27.25, - o: 27.125, - c: 26.875, - }, - { - date: new Date(1994, 2, 18), - l: 26.375, - h: 27.125, - o: 27.0, - c: 27.125, - }, - { - date: new Date(1994, 2, 21), - l: 26.75, - h: 27.875, - o: 26.875, - c: 27.75, - }, - { - date: new Date(1994, 2, 22), - l: 26.75, - h: 28.375, - o: 27.5, - c: 27.0, - }, - { - date: new Date(1994, 2, 23), - l: 26.875, - h: 28.125, - o: 27.0, - c: 28.0, - }, - { - date: new Date(1994, 2, 24), - l: 26.25, - h: 27.875, - o: 27.75, - c: 27.625, - }, - { - date: new Date(1994, 2, 25), - l: 27.5, - h: 28.75, - o: 27.75, - c: 28.0, - }, - { - date: new Date(1994, 2, 28), - l: 25.75, - h: 28.25, - o: 28.0, - c: 27.25, - }, - { - date: new Date(1994, 2, 29), - l: 26.375, - h: 27.5, - o: 27.5, - c: 26.875, - }, - { - date: new Date(1994, 2, 30), - l: 25.75, - h: 27.5, - o: 26.375, - c: 26.25, - }, - { - date: new Date(1994, 2, 31), - l: 24.75, - h: 27.0, - o: 26.5, - c: 25.25, - }, -]; +function generateStockData(startDate, days) { + let seed = 12345; + function random() { + seed = (seed * 16807) % 2147483647; + return (seed - 1) / 2147483646; + } + function randomOffset(amplitude) { + return (random() - 0.5) * amplitude; + } + const data = []; + const current = new Date(startDate); + let price = 25; + while (data.length < days) { + const day = current.getDay(); + if (day !== 0 && day !== 6) { + const open = price + randomOffset(1.5); + const close = open + randomOffset(1.5); + const high = Math.max(open, close) + random() * 0.8; + const low = Math.min(open, close) - random() * 0.8; + data.push({ + date: new Date(current), + o: +open.toFixed(2), + h: +high.toFixed(2), + l: +low.toFixed(2), + c: +close.toFixed(2), + }); + price = close; + } + current.setDate(current.getDate() + 1); + } + return data; +} +export const dataSource = generateStockData(new Date(1994, 3, 1), 65); diff --git a/apps/demos/Demos/Charts/Stock/Vue/App.vue b/apps/demos/Demos/Charts/Stock/Vue/App.vue index 5d6a15657757..cbc0707bedb4 100644 --- a/apps/demos/Demos/Charts/Stock/Vue/App.vue +++ b/apps/demos/Demos/Charts/Stock/Vue/App.vue @@ -1,4 +1,31 @@ <template> + <DxToolbar> + <DxItem + :options="panLeftButtonOptions" + location="after" + widget="dxButton" + /> + <DxItem + :options="panRightButtonOptions" + location="after" + widget="dxButton" + /> + <DxItem + :options="zoomInButtonOptions" + location="after" + widget="dxButton" + /> + <DxItem + :options="zoomOutButtonOptions" + location="after" + widget="dxButton" + /> + <DxItem + :options="resetZoomButtonOptions" + location="after" + widget="dxButton" + /> + </DxToolbar> <DxChart id="chart" :data-source="dataSource" @@ -17,10 +44,34 @@ > <DxReduction color="red"/> </DxSeries> - <DxArgumentAxis :workdays-only="true"> - <DxLabel format="shortDate"/> + <DxZoomAndPan + :drag-to-zoom="true" + :allow-mouse-wheel="true" + argument-axis="both" + /> + <DxScrollBar + :visible="true" + position="bottom" + /> + <DxLegend + vertical-alignment="bottom" + horizontal-alignment="center" + /> + <DxArgumentAxis :min-visual-range-length="minVisualRangeLength"> + <DxVisualRange + v-model:start-value="startValue" + v-model:end-value="endValue" + /> + <DxLabel + :rotation-angle="30" + format="MMM dd" + overlapping-behavior="rotate" + /> </DxArgumentAxis> - <DxValueAxis :tick-interval="1"> + <DxValueAxis + :tick-interval="1" + position="right" + > <DxTitle text="US dollars"/> <DxLabel> <DxFormat @@ -32,32 +83,134 @@ <DxExport :enabled="true"/> <DxTooltip :enabled="true" + :format="{ type: 'currency', precision: 2 }" :customize-tooltip="customizeTooltip" location="edge" /> </DxChart> </template> <script setup lang="ts"> +import { ref } from 'vue'; import DxChart, { DxCommonSeriesSettings, DxSeries, DxReduction, DxArgumentAxis, + DxVisualRange, DxLabel, DxFormat, DxValueAxis, DxTitle, + DxZoomAndPan, + DxScrollBar, + DxLegend, DxExport, DxTooltip, + type DxChartTypes, } from 'devextreme-vue/chart'; +import type { DxButtonTypes } from 'devextreme-vue/button'; +import DxToolbar, { DxItem } from 'devextreme-vue/toolbar'; import { dataSource } from './data.ts'; -const customizeTooltip = (pointInfo: any) => ({ - text: `Open: $${pointInfo.openValue}<br/> - Close: $${pointInfo.closeValue}<br/> - High: $${pointInfo.highValue}<br/> - Low: $${pointInfo.lowValue}<br/>`, +const dayMs = 24 * 60 * 60 * 1000; +const panStepFraction = 0.25; +const zoomFactor = 0.75; +const minVisualRangeLength = { days: 3 }; +const minVisibleSpanMs = minVisualRangeLength.days * dayMs; + +const initialEnd = new Date(1994, 3, 29, 12); + +const dataMin = dataSource[0].date.getTime(); +const dataMax = dataSource[dataSource.length - 1].date.getTime(); +const rangeMin = dataMin - dayMs / 2; +const rangeMax = dataMax + dayMs / 2; +const initialStart = new Date(rangeMin); + +const startValue = ref(initialStart); +const endValue = ref(initialEnd); + +const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max); + +const getRange = () => ({ + start: startValue.value.getTime(), + end: endValue.value.getTime(), }); + +const setRange = (start: number, end: number) => { + const span = clamp(end - start, minVisibleSpanMs, rangeMax - rangeMin); + const center = (start + end) / 2; + const newStart = clamp(center - span / 2, rangeMin, rangeMax - span); + + startValue.value = new Date(newStart); + endValue.value = new Date(newStart + span); +}; + +const pan = (direction: number) => { + const { start, end } = getRange(); + const shift = (end - start) * panStepFraction * direction; + setRange(start + shift, end + shift); +}; + +const zoom = (factor: number) => { + const { start, end } = getRange(); + const center = (start + end) / 2; + const half = ((end - start) * factor) / 2; + setRange(center - half, center + half); +}; + +const resetZoom = () => { + setRange(initialStart.getTime(), initialEnd.getTime()); +}; + +const panLeftButtonOptions: DxButtonTypes.Properties = { + icon: 'chevronleft', + hint: 'Pan Left', + elementAttr: { 'aria-label': 'Pan Left' }, + onClick: () => pan(-1), +}; + +const panRightButtonOptions: DxButtonTypes.Properties = { + icon: 'chevronright', + hint: 'Pan Right', + elementAttr: { 'aria-label': 'Pan Right' }, + onClick: () => pan(1), +}; + +const zoomInButtonOptions: DxButtonTypes.Properties = { + icon: 'plus', + hint: 'Zoom In', + elementAttr: { 'aria-label': 'Zoom In' }, + onClick: () => zoom(zoomFactor), +}; + +const zoomOutButtonOptions: DxButtonTypes.Properties = { + icon: 'minus', + hint: 'Zoom Out', + elementAttr: { 'aria-label': 'Zoom Out' }, + onClick: () => zoom(1 / zoomFactor), +}; + +const resetZoomButtonOptions: DxButtonTypes.Properties = { + icon: 'refresh', + hint: 'Reset Zoom', + elementAttr: { 'aria-label': 'Reset Zoom' }, + onClick: resetZoom, +}; + +const customizeTooltip = (pointInfo: DxChartTypes.PointInfo) => { + if (!('openValue' in pointInfo)) { + return { text: '' }; + } + + return { + text: [ + `Open: ${pointInfo.openValueText}`, + `Close: ${pointInfo.closeValueText}`, + `High: ${pointInfo.highValueText}`, + `Low: ${pointInfo.lowValueText}`, + ].join('<br/>'), + }; +}; </script> <style> #chart { diff --git a/apps/demos/Demos/Charts/Stock/Vue/data.ts b/apps/demos/Demos/Charts/Stock/Vue/data.ts index 29303780dd9e..1130d8f41409 100644 --- a/apps/demos/Demos/Charts/Stock/Vue/data.ts +++ b/apps/demos/Demos/Charts/Stock/Vue/data.ts @@ -1,139 +1,48 @@ -export const dataSource = [{ - date: new Date(1994, 2, 1), - l: 24.00, - h: 25.00, - o: 25.00, - c: 24.875, -}, { - date: new Date(1994, 2, 2), - l: 23.625, - h: 25.125, - o: 24.00, - c: 24.875, -}, { - date: new Date(1994, 2, 3), - l: 26.25, - h: 28.25, - o: 26.75, - c: 27.00, -}, { - date: new Date(1994, 2, 4), - l: 26.50, - h: 27.875, - o: 26.875, - c: 27.25, -}, { - date: new Date(1994, 2, 7), - l: 26.375, - h: 27.50, - o: 27.375, - c: 26.75, -}, { - date: new Date(1994, 2, 8), - l: 25.75, - h: 26.875, - o: 26.75, - c: 26.00, -}, { - date: new Date(1994, 2, 9), - l: 25.75, - h: 26.75, - o: 26.125, - c: 26.25, -}, { - date: new Date(1994, 2, 10), - l: 25.75, - h: 26.375, - o: 26.375, - c: 25.875, -}, { - date: new Date(1994, 2, 11), - l: 24.875, - h: 26.125, - o: 26.00, - c: 25.375, -}, { - date: new Date(1994, 2, 14), - l: 25.125, - h: 26.00, - o: 25.625, - c: 25.75, -}, { - date: new Date(1994, 2, 15), - l: 25.875, - h: 26.625, - o: 26.125, - c: 26.375, -}, { - date: new Date(1994, 2, 16), - l: 26.25, - h: 27.375, - o: 26.25, - c: 27.25, -}, { - date: new Date(1994, 2, 17), - l: 26.875, - h: 27.25, - o: 27.125, - c: 26.875, -}, { - date: new Date(1994, 2, 18), - l: 26.375, - h: 27.125, - o: 27.00, - c: 27.125, -}, { - date: new Date(1994, 2, 21), - l: 26.75, - h: 27.875, - o: 26.875, - c: 27.75, -}, { - date: new Date(1994, 2, 22), - l: 26.75, - h: 28.375, - o: 27.50, - c: 27.00, -}, { - date: new Date(1994, 2, 23), - l: 26.875, - h: 28.125, - o: 27.00, - c: 28.00, -}, { - date: new Date(1994, 2, 24), - l: 26.25, - h: 27.875, - o: 27.75, - c: 27.625, -}, { - date: new Date(1994, 2, 25), - l: 27.50, - h: 28.75, - o: 27.75, - c: 28.00, -}, { - date: new Date(1994, 2, 28), - l: 25.75, - h: 28.25, - o: 28.00, - c: 27.25, -}, { - date: new Date(1994, 2, 29), - l: 26.375, - h: 27.50, - o: 27.50, - c: 26.875, -}, { - date: new Date(1994, 2, 30), - l: 25.75, - h: 27.50, - o: 26.375, - c: 26.25, -}, { - date: new Date(1994, 2, 31), - l: 24.75, - h: 27.00, - o: 26.50, - c: 25.25, -}]; +export interface StockPrice { + date: Date; + l: number; + h: number; + o: number; + c: number; +} + +function generateStockData(startDate: Date, days: number): StockPrice[] { + let seed = 12345; + + function random(): number { + seed = (seed * 16807) % 2147483647; + return (seed - 1) / 2147483646; + } + + function randomOffset(amplitude: number): number { + return (random() - 0.5) * amplitude; + } + + const data: StockPrice[] = []; + const current = new Date(startDate); + let price = 25; + + while (data.length < days) { + const day = current.getDay(); + if (day !== 0 && day !== 6) { + const open = price + randomOffset(1.5); + const close = open + randomOffset(1.5); + const high = Math.max(open, close) + random() * 0.8; + const low = Math.min(open, close) - random() * 0.8; + + data.push({ + date: new Date(current), + o: +open.toFixed(2), + h: +high.toFixed(2), + l: +low.toFixed(2), + c: +close.toFixed(2), + }); + + price = close; + } + current.setDate(current.getDate() + 1); + } + return data; +} + +export const dataSource = generateStockData(new Date(1994, 3, 1), 65); diff --git a/apps/demos/Demos/Charts/Stock/description.md b/apps/demos/Demos/Charts/Stock/description.md index c4d6d9706a27..2fa78141aab0 100644 --- a/apps/demos/Demos/Charts/Stock/description.md +++ b/apps/demos/Demos/Charts/Stock/description.md @@ -7,5 +7,8 @@ Open and close prices are displayed as left and right tick marks. To specify the To reflect changes on the market, the Chart component compares the price of every point with the price of the previous point. The Chart paints the points whose price has decreased in a specific [reduction color](/Documentation/ApiReference/UI_Components/dxChart/Configuration/series/reduction/#color). To switch between high, low, open or close prices of points as sources for data comparison, use the [series](/Documentation/ApiReference/UI_Components/dxChart/Configuration/series/).[reduction](/Documentation/ApiReference/UI_Components/dxChart/Configuration/series/reduction/).[level](/Documentation/ApiReference/UI_Components/dxChart/Configuration/series/reduction/#level) property. -Additionally, use the [tooltip](/Documentation/ApiReference/UI_Components/dxChart/Configuration/tooltip/).[contentTemplate](/Documentation/ApiReference/UI_Components/dxChart/Configuration/tooltip/#contentTemplate) property to declare a custom tooltip that shows prices. You can also enable the [argumentAxis](/Documentation/ApiReference/UI_Components/dxChart/Configuration/argumentAxis/).[workdaysOnly](/Documentation/ApiReference/UI_Components/dxChart/Configuration/argumentAxis/#workdaysOnly) property to display only the work days on the argument axis. +Additionally, use the [tooltip](/Documentation/ApiReference/UI_Components/dxChart/Configuration/tooltip/).[customizeTooltip](/Documentation/ApiReference/UI_Components/dxChart/Configuration/tooltip/#customizeTooltip) function to customize a tooltip that shows prices. +Use the [zoomAndPan](/Documentation/ApiReference/UI_Components/dxChart/Configuration/zoomAndPan/) object to enable zooming and panning along the argument axis. Since this demo enables [dragToZoom](/Documentation/ApiReference/UI_Components/dxChart/Configuration/zoomAndPan/#dragToZoom), drag across the chart to zoom in. Hold **Shift** (the default [panKey](/Documentation/ApiReference/UI_Components/dxChart/Configuration/zoomAndPan/#panKey)) while dragging to pan. You can also zoom with the mouse wheel or use the [scrollBar](/Documentation/ApiReference/UI_Components/dxChart/Configuration/scrollBar/).[visible](/Documentation/ApiReference/UI_Components/dxChart/Configuration/scrollBar/#visible) property to display a scroll bar. Set its [position](/Documentation/ApiReference/UI_Components/dxChart/Configuration/scrollBar/#position) to **bottom** to place it below the chart. + +The [argumentAxis](/Documentation/ApiReference/UI_Components/dxChart/Configuration/argumentAxis/).[visualRange](/Documentation/ApiReference/UI_Components/dxChart/Configuration/argumentAxis/visualRange/) property specifies the visible date range. In this demo, toolbar buttons update this range to pan left or right, zoom in or out, and reset the chart to its initial range. diff --git a/apps/demos/Demos/Charts/Stock/jQuery/data.js b/apps/demos/Demos/Charts/Stock/jQuery/data.js index 0c54305cbfbd..dfb1969617d6 100644 --- a/apps/demos/Demos/Charts/Stock/jQuery/data.js +++ b/apps/demos/Demos/Charts/Stock/jQuery/data.js @@ -1,139 +1,38 @@ -const dataSource = [{ - date: new Date(1994, 2, 1), - l: 24.00, - h: 25.00, - o: 25.00, - c: 24.875, -}, { - date: new Date(1994, 2, 2), - l: 23.625, - h: 25.125, - o: 24.00, - c: 24.875, -}, { - date: new Date(1994, 2, 3), - l: 26.25, - h: 28.25, - o: 26.75, - c: 27.00, -}, { - date: new Date(1994, 2, 4), - l: 26.50, - h: 27.875, - o: 26.875, - c: 27.25, -}, { - date: new Date(1994, 2, 7), - l: 26.375, - h: 27.50, - o: 27.375, - c: 26.75, -}, { - date: new Date(1994, 2, 8), - l: 25.75, - h: 26.875, - o: 26.75, - c: 26.00, -}, { - date: new Date(1994, 2, 9), - l: 25.75, - h: 26.75, - o: 26.125, - c: 26.25, -}, { - date: new Date(1994, 2, 10), - l: 25.75, - h: 26.375, - o: 26.375, - c: 25.875, -}, { - date: new Date(1994, 2, 11), - l: 24.875, - h: 26.125, - o: 26.00, - c: 25.375, -}, { - date: new Date(1994, 2, 14), - l: 25.125, - h: 26.00, - o: 25.625, - c: 25.75, -}, { - date: new Date(1994, 2, 15), - l: 25.875, - h: 26.625, - o: 26.125, - c: 26.375, -}, { - date: new Date(1994, 2, 16), - l: 26.25, - h: 27.375, - o: 26.25, - c: 27.25, -}, { - date: new Date(1994, 2, 17), - l: 26.875, - h: 27.25, - o: 27.125, - c: 26.875, -}, { - date: new Date(1994, 2, 18), - l: 26.375, - h: 27.125, - o: 27.00, - c: 27.125, -}, { - date: new Date(1994, 2, 21), - l: 26.75, - h: 27.875, - o: 26.875, - c: 27.75, -}, { - date: new Date(1994, 2, 22), - l: 26.75, - h: 28.375, - o: 27.50, - c: 27.00, -}, { - date: new Date(1994, 2, 23), - l: 26.875, - h: 28.125, - o: 27.00, - c: 28.00, -}, { - date: new Date(1994, 2, 24), - l: 26.25, - h: 27.875, - o: 27.75, - c: 27.625, -}, { - date: new Date(1994, 2, 25), - l: 27.50, - h: 28.75, - o: 27.75, - c: 28.00, -}, { - date: new Date(1994, 2, 28), - l: 25.75, - h: 28.25, - o: 28.00, - c: 27.25, -}, { - date: new Date(1994, 2, 29), - l: 26.375, - h: 27.50, - o: 27.50, - c: 26.875, -}, { - date: new Date(1994, 2, 30), - l: 25.75, - h: 27.50, - o: 26.375, - c: 26.25, -}, { - date: new Date(1994, 2, 31), - l: 24.75, - h: 27.00, - o: 26.50, - c: 25.25, -}]; +function generateStockData(startDate, days) { + let seed = 12345; + + function random() { + seed = (seed * 16807) % 2147483647; + return (seed - 1) / 2147483646; + } + + function randomOffset(amplitude) { + return (random() - 0.5) * amplitude; + } + + const data = []; + const current = new Date(startDate); + let price = 25; + + while (data.length < days) { + const day = current.getDay(); + if (day !== 0 && day !== 6) { + const open = price + randomOffset(1.5); + const close = open + randomOffset(1.5); + const high = Math.max(open, close) + random() * 0.8; + const low = Math.min(open, close) - random() * 0.8; + + data.push({ + date: new Date(current), + o: +open.toFixed(2), + h: +high.toFixed(2), + l: +low.toFixed(2), + c: +close.toFixed(2), + }); + + price = close; + } + current.setDate(current.getDate() + 1); + } + return data; +} diff --git a/apps/demos/Demos/Charts/Stock/jQuery/index.html b/apps/demos/Demos/Charts/Stock/jQuery/index.html index 06da1058e48a..5c2280d22490 100644 --- a/apps/demos/Demos/Charts/Stock/jQuery/index.html +++ b/apps/demos/Demos/Charts/Stock/jQuery/index.html @@ -14,6 +14,7 @@ </head> <body class="dx-viewport"> <div class="demo-container"> + <div id="toolbar"></div> <div id="chart"></div> </div> </body> diff --git a/apps/demos/Demos/Charts/Stock/jQuery/index.js b/apps/demos/Demos/Charts/Stock/jQuery/index.js index 3ee2aaefbda1..421a9c7b8a93 100644 --- a/apps/demos/Demos/Charts/Stock/jQuery/index.js +++ b/apps/demos/Demos/Charts/Stock/jQuery/index.js @@ -1,55 +1,163 @@ $(() => { - $('#chart').dxChart({ + const dayMs = 24 * 60 * 60 * 1000; + + const panStepFraction = 0.25; + const zoomFactor = 0.75; + const minVisualRangeLength = { days: 3 }; + const minVisibleSpanMs = minVisualRangeLength.days * dayMs; + + const initialEnd = new Date(1994, 3, 29, 12); + + const data = generateStockData(new Date(1994, 3, 1), 65); + const dataMin = data[0].date.getTime(); + const dataMax = data[data.length - 1].date.getTime(); + const rangeMin = dataMin - dayMs / 2; + const rangeMax = dataMax + dayMs / 2; + const initialStart = new Date(rangeMin); + + const chart = $('#chart').dxChart({ title: 'Stock Price', - dataSource, - commonSeriesSettings: { - argumentField: 'date', - type: 'stock', + dataSource: data, + commonSeriesSettings: { argumentField: 'date', type: 'stock' }, + series: [{ + name: 'E-Mart', + openValueField: 'o', + highValueField: 'h', + lowValueField: 'l', + closeValueField: 'c', + reduction: { color: 'red' }, + }], + zoomAndPan: { + argumentAxis: 'both', + dragToZoom: true, + allowMouseWheel: true, }, - series: [ - { - name: 'E-Mart', - openValueField: 'o', - highValueField: 'h', - lowValueField: 'l', - closeValueField: 'c', - reduction: { - color: 'red', - }, - }, - ], + scrollBar: { visible: true, position: 'bottom' }, + legend: { verticalAlignment: 'bottom', horizontalAlignment: 'center' }, valueAxis: { + position: 'right', tickInterval: 1, - title: { - text: 'US dollars', - }, - label: { - format: { - type: 'currency', - precision: 0, - }, - }, + title: { text: 'US dollars' }, + label: { format: { type: 'currency', precision: 0 } }, }, argumentAxis: { - workdaysOnly: true, + minVisualRangeLength, + visualRange: { startValue: initialStart, endValue: initialEnd }, label: { - format: 'shortDate', + format: 'MMM dd', + overlappingBehavior: 'rotate', + rotationAngle: 30, }, }, - export: { - enabled: true, - }, + export: { enabled: true }, tooltip: { enabled: true, location: 'edge', - customizeTooltip(arg) { + format: { type: 'currency', precision: 2 }, + customizeTooltip({ + openValueText, closeValueText, highValueText, lowValueText, + }) { return { - text: `Open: $${arg.openValue}<br/>` - + `Close: $${arg.closeValue}<br/>` - + `High: $${arg.highValue}<br/>` - + `Low: $${arg.lowValue}<br/>`, + text: [ + `Open: ${openValueText}`, + `Close: ${closeValueText}`, + `High: ${highValueText}`, + `Low: ${lowValueText}`, + ].join('<br/>'), }; }, }, + }).dxChart('instance'); + + function clamp(value, min, max) { + return Math.min(Math.max(value, min), max); + } + + function getRange() { + const { startValue, endValue } = chart.getArgumentAxis().visualRange(); + return { start: startValue.getTime(), end: endValue.getTime() }; + } + + function setRange(start, end) { + const span = clamp(end - start, minVisibleSpanMs, rangeMax - rangeMin); + const center = (start + end) / 2; + const newStart = clamp(center - span / 2, rangeMin, rangeMax - span); + + chart.getArgumentAxis().visualRange({ + startValue: new Date(newStart), + endValue: new Date(newStart + span), + }); + } + + function pan(direction) { + const { start, end } = getRange(); + const shift = (end - start) * panStepFraction * direction; + setRange(start + shift, end + shift); + } + + function zoom(factor) { + const { start, end } = getRange(); + const center = (start + end) / 2; + const half = ((end - start) * factor) / 2; + setRange(center - half, center + half); + } + + function resetZoom() { + setRange(initialStart.getTime(), initialEnd.getTime()); + } + + $('#toolbar').dxToolbar({ + items: [ + { + location: 'after', + widget: 'dxButton', + options: { + icon: 'chevronleft', + hint: 'Pan Left', + elementAttr: { 'aria-label': 'Pan Left' }, + onClick: () => pan(-1), + }, + }, + { + location: 'after', + widget: 'dxButton', + options: { + icon: 'chevronright', + hint: 'Pan Right', + elementAttr: { 'aria-label': 'Pan Right' }, + onClick: () => pan(1), + }, + }, + { + location: 'after', + widget: 'dxButton', + options: { + icon: 'plus', + hint: 'Zoom In', + elementAttr: { 'aria-label': 'Zoom In' }, + onClick: () => zoom(zoomFactor), + }, + }, + { + location: 'after', + widget: 'dxButton', + options: { + icon: 'minus', + hint: 'Zoom Out', + elementAttr: { 'aria-label': 'Zoom Out' }, + onClick: () => zoom(1 / zoomFactor), + }, + }, + { + location: 'after', + widget: 'dxButton', + options: { + icon: 'refresh', + hint: 'Reset Zoom', + elementAttr: { 'aria-label': 'Reset Zoom' }, + onClick: resetZoom, + }, + }, + ], }); }); diff --git a/apps/demos/menuMeta.json b/apps/demos/menuMeta.json index 4ceaddfd4ddb..05f74be07fce 100644 --- a/apps/demos/menuMeta.json +++ b/apps/demos/menuMeta.json @@ -2776,10 +2776,10 @@ "Name": "Stock", "Widget": "Charts", "MvcAdditionalFiles": [ - "/Models/SampleData/FinancialData.cs" + "/Models/SampleData/StockChartData.cs" ], "DemoType": "Viz", - "Equivalents": "workdaysOnly, tickInterval, stock price, reduction" + "Equivalents": "zoomAndPan, scrollBar, visualRange, tickInterval, stock price, reduction" }, { "Title": "Candlestick", diff --git a/apps/demos/testing/etalons/Charts-Stock (fluent-next.blue.light).png b/apps/demos/testing/etalons/Charts-Stock (fluent-next.blue.light).png index 10704465c3b1..1b99acce9908 100644 Binary files a/apps/demos/testing/etalons/Charts-Stock (fluent-next.blue.light).png and b/apps/demos/testing/etalons/Charts-Stock (fluent-next.blue.light).png differ diff --git a/apps/demos/testing/etalons/Charts-Stock (fluent.blue.light).png b/apps/demos/testing/etalons/Charts-Stock (fluent.blue.light).png index 3932b5bca840..91a502aada2f 100644 Binary files a/apps/demos/testing/etalons/Charts-Stock (fluent.blue.light).png and b/apps/demos/testing/etalons/Charts-Stock (fluent.blue.light).png differ diff --git a/apps/demos/testing/etalons/Charts-Stock (material.blue.light).png b/apps/demos/testing/etalons/Charts-Stock (material.blue.light).png index 94c83f45d2e8..8836c8c608d8 100644 Binary files a/apps/demos/testing/etalons/Charts-Stock (material.blue.light).png and b/apps/demos/testing/etalons/Charts-Stock (material.blue.light).png differ