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..489d11f83074 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 @@ + + + + + + + + + + + + - + + + + + - - + + this.pan(-1), + }; + + panRightButtonOptions: DxButtonTypes.Properties = { + icon: 'chevronright', + hint: 'Pan Right', + onClick: () => this.pan(1), + }; + + zoomInButtonOptions: DxButtonTypes.Properties = { + icon: 'plus', + hint: 'Zoom In', + onClick: () => this.zoom(zoomFactor), + }; + + zoomOutButtonOptions: DxButtonTypes.Properties = { + icon: 'minus', + hint: 'Zoom Out', + onClick: () => this.zoom(1 / zoomFactor), + }; + + resetZoomButtonOptions: DxButtonTypes.Properties = { + icon: 'refresh', + hint: '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(); + } + + 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.dataMax - this.dataMin); + const center = (start + end) / 2; + const newStart = clamp(center - span / 2, this.dataMin, this.dataMax - 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}
` 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..4d776069b5dd 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,12 +8,41 @@ 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 minVisibleSpanMs = 3 * dayMs; +const minVisualRangeLength = { days: 3 }; + +const initialStart = new Date(1994, 3, 14, 12); +const initialEnd = new Date(1994, 3, 29, 12); + +const dataMin = dataSource[0].date.getTime(); +const dataMax = dataSource[dataSource.length - 1].date.getTime(); + +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 customizeTooltip(arg: ChartTypes.PointInfo): Record { if (!('openValue' in arg)) { return { text: '' }; @@ -28,34 +57,129 @@ function customizeTooltip(arg: ChartTypes.PointInfo): Record { } function App() { + const [visualRange, setVisualRange] = useState({ + startValue: initialStart, + endValue: initialEnd, + }); + + const onVisualRangeChange = useCallback((range: VisualRange | (Date | number | string)[]) => { + setVisualRange(range as VisualRange); + }, []); + + const setRange = useCallback((start: number, end: number) => { + const span = clamp(end - start, minVisibleSpanMs, dataMax - dataMin); + const center = (start + end) / 2; + const newStart = clamp(center - span / 2, dataMin, dataMax - span); + + setVisualRange({ + startValue: new Date(newStart), + endValue: new Date(newStart + span), + }); + }, []); + + const pan = useCallback((direction: number) => { + const { start, end } = getRange(visualRange); + const shift = (end - start) * panStepFraction * direction; + setRange(start + shift, end + shift); + }, [visualRange, setRange]); + + const zoom = useCallback((factor: number) => { + const { start, end } = getRange(visualRange); + const center = (start + end) / 2; + const half = ((end - start) * factor) / 2; + setRange(center - half, center + half); + }, [visualRange, setRange]); + + const resetZoom = useCallback(() => { + setRange(initialStart.getTime(), initialEnd.getTime()); + }, [setRange]); + + const panLeftButtonOptions = useMemo(() => ({ + icon: 'chevronleft', + hint: 'Pan Left', + onClick: () => pan(-1), + }), [pan]); + + const panRightButtonOptions = useMemo(() => ({ + icon: 'chevronright', + hint: 'Pan Right', + onClick: () => pan(1), + }), [pan]); + + const zoomInButtonOptions = useMemo(() => ({ + icon: 'plus', + hint: 'Zoom In', + onClick: () => zoom(zoomFactor), + }), [zoom]); + + const zoomOutButtonOptions = useMemo(() => ({ + icon: 'minus', + hint: 'Zoom Out', + onClick: () => zoom(1 / zoomFactor), + }), [zoom]); + + const resetZoomButtonOptions = useMemo(() => ({ + icon: 'refresh', + hint: '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} + 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..083d15f11825 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,11 +8,33 @@ 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 minVisibleSpanMs = 3 * dayMs; +const minVisualRangeLength = { days: 3 }; +const initialStart = new Date(1994, 3, 14, 12); +const initialEnd = new Date(1994, 3, 29, 12); +const dataMin = dataSource[0].date.getTime(); +const dataMax = dataSource[dataSource.length - 1].date.getTime(); +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 customizeTooltip(arg) { if (!('openValue' in arg)) { return { text: '' }; @@ -25,44 +47,173 @@ function customizeTooltip(arg) { }; } function App() { + const [visualRange, setVisualRange] = useState({ + startValue: initialStart, + endValue: initialEnd, + }); + const onVisualRangeChange = useCallback((range) => { + setVisualRange(range); + }, []); + const setRange = useCallback((start, end) => { + const span = clamp(end - start, minVisibleSpanMs, dataMax - dataMin); + const center = (start + end) / 2; + const newStart = clamp(center - span / 2, dataMin, dataMax - span); + setVisualRange({ + startValue: new Date(newStart), + endValue: new Date(newStart + span), + }); + }, []); + const pan = useCallback( + (direction) => { + const { start, end } = getRange(visualRange); + const shift = (end - start) * panStepFraction * direction; + setRange(start + shift, end + shift); + }, + [visualRange, setRange], + ); + const zoom = useCallback( + (factor) => { + const { start, end } = getRange(visualRange); + const center = (start + end) / 2; + const half = ((end - start) * factor) / 2; + setRange(center - half, center + half); + }, + [visualRange, setRange], + ); + const resetZoom = useCallback(() => { + setRange(initialStart.getTime(), initialEnd.getTime()); + }, [setRange]); + const panLeftButtonOptions = useMemo( + () => ({ + icon: 'chevronleft', + hint: 'Pan Left', + onClick: () => pan(-1), + }), + [pan], + ); + const panRightButtonOptions = useMemo( + () => ({ + icon: 'chevronright', + hint: 'Pan Right', + onClick: () => pan(1), + }), + [pan], + ); + const zoomInButtonOptions = useMemo( + () => ({ + icon: 'plus', + hint: 'Zoom In', + onClick: () => zoom(zoomFactor), + }), + [zoom], + ); + const zoomOutButtonOptions = useMemo( + () => ({ + icon: 'minus', + hint: 'Zoom Out', + onClick: () => zoom(1 / zoomFactor), + }), + [zoom], + ); + const resetZoomButtonOptions = useMemo( + () => ({ + icon: 'refresh', + hint: '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} + 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..d09a8271510a 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="{ days: 3 }"> + <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 @@ -38,26 +89,117 @@ </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/> +const dayMs = 24 * 60 * 60 * 1000; +const panStepFraction = 0.25; +const zoomFactor = 0.75; +const minVisibleSpanMs = 3 * dayMs; + +const initialStart = new Date(1994, 3, 14, 12); +const initialEnd = new Date(1994, 3, 29, 12); + +const dataMin = dataSource[0].date.getTime(); +const dataMax = dataSource[dataSource.length - 1].date.getTime(); + +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, dataMax - dataMin); + const center = (start + end) / 2; + const newStart = clamp(center - span / 2, dataMin, dataMax - 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', + onClick: () => pan(-1), +}; + +const panRightButtonOptions: DxButtonTypes.Properties = { + icon: 'chevronright', + hint: 'Pan Right', + onClick: () => pan(1), +}; + +const zoomInButtonOptions: DxButtonTypes.Properties = { + icon: 'plus', + hint: 'Zoom In', + onClick: () => zoom(zoomFactor), +}; + +const zoomOutButtonOptions: DxButtonTypes.Properties = { + icon: 'minus', + hint: 'Zoom Out', + onClick: () => zoom(1 / zoomFactor), +}; + +const resetZoomButtonOptions: DxButtonTypes.Properties = { + icon: 'refresh', + hint: 'Reset Zoom', + onClick: resetZoom, +}; + +const customizeTooltip = (pointInfo: DxChartTypes.PointInfo) => { + if (!('openValue' in pointInfo)) { + return { text: '' }; + } + + return { + text: `Open: $${pointInfo.openValue}<br/> Close: $${pointInfo.closeValue}<br/> High: $${pointInfo.highValue}<br/> Low: $${pointInfo.lowValue}<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..6797257e3fae 100644 --- a/apps/demos/Demos/Charts/Stock/description.md +++ b/apps/demos/Demos/Charts/Stock/description.md @@ -7,5 +7,9 @@ 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. Enable the [scrollBar](/Documentation/ApiReference/UI_Components/dxChart/Configuration/scrollBar/).[visible](/Documentation/ApiReference/UI_Components/dxChart/Configuration/scrollBar/#visible) property to display a scroll bar, and 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..225cbc5634c2 100644 --- a/apps/demos/Demos/Charts/Stock/jQuery/index.js +++ b/apps/demos/Demos/Charts/Stock/jQuery/index.js @@ -1,55 +1,154 @@ $(() => { - $('#chart').dxChart({ + const dayMs = 24 * 60 * 60 * 1000; + + const panStepFraction = 0.25; + const zoomFactor = 0.75; + const minVisibleSpanMs = 3 * dayMs; + + const initialStart = new Date(1994, 3, 14, 12); + 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 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: { days: 3 }, + 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) { + customizeTooltip({ + openValue, closeValue, highValue, lowValue, + }) { return { - text: `Open: $${arg.openValue}<br/>` - + `Close: $${arg.closeValue}<br/>` - + `High: $${arg.highValue}<br/>` - + `Low: $${arg.lowValue}<br/>`, + text: [ + `Open: $${openValue}`, + `Close: $${closeValue}`, + `High: $${highValue}`, + `Low: $${lowValue}`, + ].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, dataMax - dataMin); + const center = (start + end) / 2; + const newStart = clamp(center - span / 2, dataMin, dataMax - 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', + onClick: () => pan(-1), + }, + }, + { + location: 'after', + widget: 'dxButton', + options: { + icon: 'chevronright', + hint: 'Pan Right', + onClick: () => pan(1), + }, + }, + { + location: 'after', + widget: 'dxButton', + options: { + icon: 'plus', + hint: 'Zoom In', + onClick: () => zoom(zoomFactor), + }, + }, + { + location: 'after', + widget: 'dxButton', + options: { + icon: 'minus', + hint: 'Zoom Out', + onClick: () => zoom(1 / zoomFactor), + }, + }, + { + location: 'after', + widget: 'dxButton', + options: { + icon: 'refresh', + hint: 'Reset Zoom', + onClick: resetZoom, + }, + }, + ], }); }); diff --git a/apps/demos/menuMeta.json b/apps/demos/menuMeta.json index 40db2dbcba42..76a23ad06b92 100644 --- a/apps/demos/menuMeta.json +++ b/apps/demos/menuMeta.json @@ -2756,10 +2756,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.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 diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__readonly (fluent.blue.light)_mask.png b/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__readonly (fluent.blue.light)_mask.png index bf1f8ff5c902..cbea79d4b285 100644 Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__readonly (fluent.blue.light)_mask.png and b/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__readonly (fluent.blue.light)_mask.png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__weekly (fluent.blue.light)_mask.png b/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__weekly (fluent.blue.light)_mask.png index bf1f8ff5c902..cbea79d4b285 100644 Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__weekly (fluent.blue.light)_mask.png and b/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__weekly (fluent.blue.light)_mask.png differ