Skip to content

Commit

Permalink
Merge pull request #1628 from maryia-deriv/maryia/v2-chart-scroll
Browse files Browse the repository at this point in the history
[DTRA] Maryia/feat: vertical scroll behavior updates for isVerticalScrollEnabled functionality when disabled
  • Loading branch information
balakrishna-deriv committed Aug 28, 2024
2 parents 0d6553c + bac2e9c commit 069c469
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 37 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ Props marked with `*` are **mandatory**:
| isConnectionOpened | Sets the connection status. If set, upon reconnection smartcharts will either patch missing tick data or refresh the chart, depending on granularity; if not set, it is assumed that connection is always opened. Defaults to `undefined`. |
| onMessage | SmartCharts will send notifications via this callback, should it be provided. Each notification will have the following structure: `{ text, type, category }`. |
| isAnimationEnabled | Determine whether chart animation is enabled or disabled. It may needs to be disabled for better performance. Defaults to `true`. |
| isVerticalScrollEnabled | Determine whether verticall scroll on the chart outside Y-axis is disabled. It may need to be disabled for mobile app version to scroll the page up or down instead of the chart. Defaults to `true`. |
| isVerticalScrollEnabled | Determine whether verticall scroll on the chart outside Y-axis is disabled while it is forced on the nearest scrollable parent instead. It may need to be disabled for mobile app version to scroll the page up or down instead of the chart. In this case, when scroll delta exceeds 10px, the page will be force-scrolled fully in a respective direction. Defaults to `true`. |
| showLastDigitStats | Shows last digits stats. Defaults to `false`. |
| scrollToEpoch | Scrolls the chart to the leftmost side and sets the last spot/bar as the first visible spot/bar in the chart. Also, it disables scrolling until the chart reaches the 3/4 of the width of the main pane of the chart. Defaults to `null`. |
|
Expand Down
6 changes: 5 additions & 1 deletion chart_app/lib/src/chart_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ class ChartApp {

bool _prevShowChart = false;

/// height of xAxis
double xAxisHeight = 24;

/// width of yAxis
double yAxisWidth = 60;

Expand Down Expand Up @@ -73,13 +76,14 @@ class ChartApp {
feedModel.newChart();
}

/// Calculates the width of yAxis
/// Calculates the width of yAxis and sets the height of xAxis
void calculateTickWidth() {
yAxisWidth = calculateYAxisWidth(
feedModel.ticks,
configModel.theme,
configModel.pipSize,
);
xAxisHeight = configModel.theme.gridStyle.xLabelsAreaHeight;

currentTickWidth = calculateCurrentTickWidth(
feedModel.ticks,
Expand Down
12 changes: 12 additions & 0 deletions chart_app/lib/src/interop/dart_interop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ void initDartInterop(ChartApp app) {
JsObject _exposeApp(ChartApp app) {
final JsObject jsObject = JsObject(context['Object']);

setProperty(
jsObject,
'getXAxisHeight',
allowInterop(() => app.xAxisHeight),
);

setProperty(
jsObject,
'getYAxisWidth',
Expand Down Expand Up @@ -84,6 +90,12 @@ JsObject _exposeApp(ChartApp app) {
),
);

setProperty(
jsObject,
'toggleXScrollBlock',
allowInterop(app.wrappedController.toggleXScrollBlock),
);

setProperty(
jsObject,
'toggleDataFitMode',
Expand Down
11 changes: 11 additions & 0 deletions chart_app/lib/src/misc/wrapped_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ class WrappedController {
}
}

/// Block/Unblock horizontal scroll on the chart.
// ignore: avoid_positional_boolean_parameters
void toggleXScrollBlock(bool isXScrollBlocked) {
try {
_chartController.toggleXScrollBlock
?.call(isXScrollBlocked: isXScrollBlocked);
} catch (_) {
return;
}
}

/// Scroll chart visible area to the newest data.
// ignore: avoid_positional_boolean_parameters
void toggleDataFitMode(bool dataFitMode) {
Expand Down
136 changes: 101 additions & 35 deletions src/store/ChartAdapterStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default class ChartAdapterStore {
msPerPx?: number;
drawingHoverIndex: number | undefined | null = null;
isDataFitModeEnabled = false;
isXScrollBlocked = false;
painter = new Painter();
drawingColor = 0;
isScaled = false;
Expand All @@ -41,17 +42,29 @@ export default class ChartAdapterStore {
bottomIndex: 0,
};
touchValues: {
deltaXTotal?: number;
deltaYTotal?: number;
multiTouch?: boolean;
touchIds?: number[];
x?: number;
y?: number;
yOnTouchEnd?: number;
} = {};
} = {
multiTouch: false,
deltaXTotal: 0,
deltaYTotal: 0,
touchIds: [],
x: 0,
y: 0,
};

isOverFlutterCharts = false;
enableVerticalScrollTimer?: ReturnType<typeof setTimeout>;
scrollChartParentOnTouchTimer?: ReturnType<typeof setTimeout>;
clearTouchDeltasTimer?: ReturnType<typeof setTimeout>;
enableXScrollTimer?: ReturnType<typeof setTimeout>;
enableYScrollTimer?: ReturnType<typeof setTimeout>;

constructor(mainStore: MainStore) {
makeObservable(this, {
clearTouchDeltasTimer: observable,
onMount: action.bound,
onTickHistory: action.bound,
onTouch: action.bound,
Expand All @@ -62,11 +75,12 @@ export default class ChartAdapterStore {
onQuoteAreaChanged: action.bound,
setMsPerPx: action.bound,
newChart: action.bound,
enableVerticalScrollTimer: observable,
enableXScrollTimer: observable,
enableYScrollTimer: observable,
scale: action.bound,
scrollableChartParent: computed,
scrollChartParentOnTouchTimer: observable,
toggleDataFitMode: action.bound,
toggleXScrollBlock: action.bound,
touchValues: observable,
onCrosshairMove: action.bound,
isDataFitModeEnabled: observable,
Expand Down Expand Up @@ -218,8 +232,9 @@ export default class ChartAdapterStore {
window.flutterChartElement?.removeEventListener('touchend', this.onTouch, { capture: true });
window.flutterChartElement?.removeEventListener('dblclick', this.onDoubleClick, { capture: true });
window.removeEventListener('mousemove', this.onMouseMove, { capture: true });
clearTimeout(this.enableVerticalScrollTimer);
clearTimeout(this.scrollChartParentOnTouchTimer);
clearTimeout(this.clearTouchDeltasTimer);
clearTimeout(this.enableXScrollTimer);
clearTimeout(this.enableYScrollTimer);
}

onChartLoad() {
Expand All @@ -237,38 +252,84 @@ export default class ChartAdapterStore {
}

onTouch(e: TouchEvent) {
const chartNode = this.mainStore.chart.chartNode;
// Prevent vertical scroll on the chart for touch devices by forcing scroll on a scrollable parent of the chart:
if (
chartNode &&
this.scrollableChartParent &&
!this.mainStore.state.isVerticalScrollEnabled &&
e.touches.length === 1
) {
const { pageX, screenX, screenY } = e.touches[0];
if (['touchstart', 'touchend'].includes(e.type)) {
this.touchValues = e.type === 'touchstart' ? { x: screenX, y: screenY } : { yOnTouchEnd: screenY };
} else if (e.type === 'touchmove') {
const nonScrollableAreaWidth = chartNode.offsetWidth - this.mainStore.chart.yAxisWidth;
const { left } = chartNode.getBoundingClientRect();
const chartNode = this.mainStore.chart.chartNode;
if (chartNode && this.scrollableChartParent && !this.mainStore.state.isVerticalScrollEnabled) {
if (this.touchValues.multiTouch) {
if (e.type === 'touchend') {
this.touchValues.touchIds = this.touchValues.touchIds?.filter(
id => id === e.changedTouches[0].identifier
);
this.touchValues = { multiTouch: !!this.touchValues.touchIds?.length };
}
return;
}
if (e.touches.length > 1) {
this.touchValues = { multiTouch: true };
this.touchValues.touchIds = Array.from(e.touches).map(touch => touch.identifier);
return;
}

const { pageX, pageY } = e.changedTouches[0];

if (['touchmove', 'touchend'].includes(e.type)) {
const forcedScrollAreaWidth = chartNode.offsetWidth - this.mainStore.chart.yAxisWidth;
const forcedScrollAreaHeight = chartNode.offsetHeight - this.mainStore.chart.xAxisHeight;
const { top, left } = chartNode.getBoundingClientRect();
const xCoord = pageX - left;
const yCoord = pageY - top;
const isForcedScrollArea = xCoord < forcedScrollAreaWidth && yCoord < forcedScrollAreaHeight;

if (this.touchValues.x && this.touchValues.y) {
const deltaX = Math.abs(screenX - this.touchValues.x);
const deltaY = Math.abs(screenY - this.touchValues.y);
const xDiff = this.touchValues.x - pageX;
const yDiff = this.touchValues.y - pageY;
const deltaXTotal = (this.touchValues.deltaXTotal ?? 0) + xDiff;
const deltaYTotal = (this.touchValues.deltaYTotal ?? 0) + yDiff;
const deltaX = e.type === 'touchend' ? Math.abs(deltaXTotal) : Math.abs(xDiff);
const deltaY = e.type === 'touchend' ? Math.abs(deltaYTotal) : Math.abs(yDiff);
const isVerticalScroll = deltaY > deltaX;
const x = pageX - left;
if (x < nonScrollableAreaWidth && isVerticalScroll && !this.scrollChartParentOnTouchTimer) {
this.touchValues.yOnTouchEnd = undefined;
this.scrollChartParentOnTouchTimer = setTimeout(() => {
this.scrollableChartParent?.scrollBy({
top: screenY - Number(this.touchValues.yOnTouchEnd ?? this.touchValues.y),
this.touchValues = { ...this.touchValues, deltaXTotal, deltaYTotal };

if (isForcedScrollArea && isVerticalScroll) {
const shouldForceMaxScroll =
Math.abs(Number(this.touchValues.deltaYTotal)) > 10 && e.type === 'touchend';
if (!this.isXScrollBlocked) this.toggleXScrollBlock();
if (shouldForceMaxScroll) {
// handling max scroll on quick swipe
this.scrollableChartParent?.scrollTo({
top:
Number(this.touchValues.deltaYTotal) < 0
? 0
: this.scrollableChartParent.scrollHeight,
behavior: 'smooth',
});
this.scrollChartParentOnTouchTimer = undefined;
}, 300);
} else if (e.type === 'touchmove') {
// handling slow scroll
this.scrollableChartParent?.scrollBy({
top: yDiff,
});
if (!this.clearTouchDeltasTimer) {
this.clearTouchDeltasTimer = setTimeout(() => {
// clearing total deltas to avoid triggering max scroll after the slow scroll
this.touchValues = { ...this.touchValues, deltaYTotal: 0, deltaXTotal: 0 };
this.clearTouchDeltasTimer = undefined;
}, 100);
}
}
}
}
this.touchValues = { x: screenX, y: screenY };
this.touchValues = { ...this.touchValues, x: pageX, y: pageY };
if (e.type === 'touchend' && this.isXScrollBlocked) {
this.enableXScrollTimer = setTimeout(() => {
this.toggleXScrollBlock(false);
}, 100);
}
}
if (['touchstart', 'touchend'].includes(e.type)) {
this.touchValues =
e.type === 'touchstart'
? { x: pageX, y: pageY }
: { deltaYTotal: this.touchValues.deltaYTotal, deltaXTotal: this.touchValues.deltaXTotal };
}
}
}
Expand All @@ -284,11 +345,11 @@ export default class ChartAdapterStore {
const isVerticalScroll = e.deltaY && e.deltaX === 0;
const x = e.pageX - left;
if (x < nonScrollableAreaWidth && isVerticalScroll) {
if (this.enableVerticalScrollTimer) return;
if (this.enableYScrollTimer) return;
chartNode.style.pointerEvents = 'none';
this.enableVerticalScrollTimer = setTimeout(() => {
this.enableYScrollTimer = setTimeout(() => {
chartNode.style.pointerEvents = 'auto';
this.enableVerticalScrollTimer = undefined;
this.enableYScrollTimer = undefined;
}, 300);
return;
}
Expand Down Expand Up @@ -474,6 +535,11 @@ export default class ChartAdapterStore {
}
}

toggleXScrollBlock = (isBlocked = true) => {
this.isXScrollBlocked = isBlocked;
window.flutterChart?.app.toggleXScrollBlock(isBlocked);
};

toggleDataFitMode = () => {
this.isDataFitModeEnabled = !this.isDataFitModeEnabled;
window.flutterChart?.app.toggleDataFitMode(this.isDataFitModeEnabled);
Expand Down
5 changes: 5 additions & 0 deletions src/store/ChartStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class ChartStore {
networkStatus: observable,
serverTime: observable,
shouldRenderDialogs: observable,
xAxisHeight: computed,
yAxisWidth: computed,
lastQuote: observable,
_initChart: action.bound,
Expand Down Expand Up @@ -134,6 +135,10 @@ class ChartStore {
return this.currentCloseQuote()?.Close;
}

get xAxisHeight(): number {
return window.flutterChart?.app.getXAxisHeight() || 24;
}

get yAxisWidth(): number {
return window.flutterChart?.app.getYAxisWidth() || 60;
}
Expand Down
2 changes: 2 additions & 0 deletions src/types/props.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ export type TDrawingToolConfig = {

export type TFlutterChart = {
app: {
getXAxisHeight: () => number;
getYAxisWidth: () => number;
getCurrentTickWidth: () => number;
newChart: (payload: TNewChartPayload) => void;
Expand All @@ -396,6 +397,7 @@ export type TFlutterChart = {
scale: (scale: number) => number;
scroll: (pxShift: number) => void;
toggleDataFitMode: (isDataFitEnabled: boolean) => void;
toggleXScrollBlock: (isXScrollBlocked: boolean) => void;
scrollToLastTick: () => void;
addOrUpdateIndicator: (config: string, index?: number) => void;
};
Expand Down

0 comments on commit 069c469

Please sign in to comment.