Skip to content

Commit

Permalink
fix: fixing scaling when data is discontinuous
Browse files Browse the repository at this point in the history
Chart will correctly allowing panning when there's missing data.
Moved timeFormat to scales to be used with other scales.
  • Loading branch information
markmcdowell committed Sep 1, 2020
1 parent afe3ba9 commit 4b20255
Show file tree
Hide file tree
Showing 10 changed files with 81 additions and 96 deletions.
5 changes: 1 addition & 4 deletions packages/axes/src/Axis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
getAxisCanvas,
GenericChartComponent,
getStrokeDasharrayCanvas,
identity,
last,
strokeDashTypes,
} from "@react-financial-charts/core";
Expand Down Expand Up @@ -205,9 +204,7 @@ const tickHelper = (props: AxisProps, scale: ScaleContinuousNumeric<number, numb
tickValues = scale.domain();
}

const baseFormat = scale.tickFormat ? scale.tickFormat(tickArguments) : identity;

const format = tickFormat === undefined ? baseFormat : (d: any) => tickFormat(d) || "";
const format = tickFormat === undefined ? scale.tickFormat(tickArguments) : (d: any) => tickFormat(d) || "";

const sign = orient === "top" || orient === "left" ? -1 : 1;
const tickSpacing = Math.max(innerTickSize, 0) + tickPadding;
Expand Down
6 changes: 3 additions & 3 deletions packages/axes/src/XAxis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as PropTypes from "prop-types";
import * as React from "react";
import { Axis } from "./Axis";

export interface XAxisProps {
export interface XAxisProps<T extends number | Date> {
readonly axisAt?: number | "top" | "bottom" | "middle";
readonly className?: string;
readonly domainClassName?: string;
Expand All @@ -25,7 +25,7 @@ export interface XAxisProps {
readonly showTickLabel?: boolean;
readonly strokeStyle?: string;
readonly strokeWidth?: number;
readonly tickFormat?: (value: number | Date) => string;
readonly tickFormat?: (value: T) => string;
readonly tickPadding?: number;
readonly tickSize?: number;
readonly tickLabelFill?: string;
Expand All @@ -39,7 +39,7 @@ export interface XAxisProps {
readonly zoomCursorClassName?: string;
}

export class XAxis extends React.Component<XAxisProps> {
export class XAxis<T extends number | Date> extends React.Component<XAxisProps<T>> {
public static defaultProps = {
axisAt: "bottom",
className: "react-financial-charts-x-axis",
Expand Down
21 changes: 12 additions & 9 deletions packages/core/src/ChartCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,7 @@ export class ChartCanvas extends React.Component<ChartCanvasProps, ChartCanvasSt
const { plotData: beforePlotData, domain } = filterData(fullData, newDomain, xAccessor, initialXScale, {
currentPlotData: this.hackyWayToStopPanBeyondBounds__plotData,
currentDomain: this.hackyWayToStopPanBeyondBounds__domain,
ignoreThresholds: true,
});

const updatedScale = initialXScale.copy().domain(domain) as
Expand All @@ -874,13 +875,15 @@ export class ChartCanvas extends React.Component<ChartCanvasProps, ChartCanvasSt
const plotData = postCalculator(beforePlotData);

const currentItem = getCurrentItem(updatedScale, xAccessor, mouseXY, plotData);

const chartConfig = getChartConfigWithUpdatedYScales(
initialChartConfig,
{ plotData, xAccessor, displayXAccessor, fullData },
updatedScale.domain(),
dy,
chartsToPan,
);

const currentCharts = getCurrentCharts(chartConfig, mouseXY);

return {
Expand All @@ -904,23 +907,23 @@ export class ChartCanvas extends React.Component<ChartCanvasProps, ChartCanvasSt
this.waitingForPanAnimationFrame = true;

this.hackyWayToStopPanBeyondBounds__plotData =
this.hackyWayToStopPanBeyondBounds__plotData || this.state.plotData;
this.hackyWayToStopPanBeyondBounds__plotData ?? this.state.plotData;
this.hackyWayToStopPanBeyondBounds__domain =
this.hackyWayToStopPanBeyondBounds__domain || this.state.xScale!.domain();
this.hackyWayToStopPanBeyondBounds__domain ?? this.state.xScale!.domain();

const state = this.panHelper(mousePosition, panStartXScale, dxdy, chartsToPan);
const newState = this.panHelper(mousePosition, panStartXScale, dxdy, chartsToPan);

this.hackyWayToStopPanBeyondBounds__plotData = state.plotData;
this.hackyWayToStopPanBeyondBounds__domain = state.xScale.domain();
this.hackyWayToStopPanBeyondBounds__plotData = newState.plotData;
this.hackyWayToStopPanBeyondBounds__domain = newState.xScale.domain();

this.panInProgress = true;

this.triggerEvent("pan", state, e);
this.triggerEvent("pan", newState, e);

this.mutableState = {
mouseXY: state.mouseXY,
currentItem: state.currentItem,
currentCharts: state.currentCharts,
mouseXY: newState.mouseXY,
currentItem: newState.currentItem,
currentCharts: newState.currentCharts,
};
requestAnimationFrame(() => {
this.waitingForPanAnimationFrame = false;
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/EventCapture.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -493,8 +493,9 @@ export class EventCapture extends React.Component<EventCaptureProps, EventCaptur
this.dx = dx;
this.dy = dy;

if (this.props.onPan !== undefined) {
this.props.onPan(mouseXY, panStartXScale, { dx, dy }, chartsToPan, e);
const { onPan } = this.props;
if (onPan !== undefined) {
onPan(mouseXY, panStartXScale, { dx, dy }, chartsToPan, e);
}
}
};
Expand Down
51 changes: 11 additions & 40 deletions packages/core/src/utils/evaluator.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { max, min } from "d3-array";
import { ScaleContinuousNumeric, ScaleTime } from "d3-scale";
import { getClosestItemIndexes, getLogger, head, isDefined, isNotDefined, last } from "../utils";

const log = getLogger("evaluator");
import { getClosestItemIndexes, head, isDefined, isNotDefined, last } from "../utils";

function getNewEnd<T, TAccessor extends number | Date>(
fallbackEnd: { lastItem: T; lastItemX: TAccessor },
Expand Down Expand Up @@ -40,7 +38,7 @@ function extentsWrapper<TDomain extends number | Date>(
inputDomain: [TDomain, TDomain],
xAccessor: (item: T) => TDomain,
initialXScale: ScaleContinuousNumeric<number, number> | ScaleTime<number, number>,
{ currentPlotData, currentDomain, fallbackStart, fallbackEnd }: any = {},
{ currentPlotData, currentDomain, fallbackStart, fallbackEnd, ignoreThresholds = false }: any = {},
) {
if (useWholeData) {
return { plotData: data, domain: inputDomain };
Expand Down Expand Up @@ -93,50 +91,21 @@ function extentsWrapper<TDomain extends number | Date>(

const chartWidth = last(xScale.range()) - head(xScale.range());

log(
// @ts-ignore
`Trying to show ${filteredData.length} points in ${width}px,` +
` I can show up to ${showMaxThreshold(width, pointsPerPxThreshold) - 1} points in that width. ` +
`Also FYI the entire chart width is ${chartWidth}px and pointsPerPxThreshold is ${pointsPerPxThreshold}`,
);

if (canShowTheseManyPeriods(width, filteredData.length, pointsPerPxThreshold, minPointsPerPxThreshold)) {
if (
(ignoreThresholds && filteredData.length > 1) ||
canShowTheseManyPeriods(width, filteredData.length, pointsPerPxThreshold, minPointsPerPxThreshold)
) {
plotData = filteredData;
domain = realInputDomain;

// @ts-ignore
log("AND IT WORKED");
} else {
if (chartWidth > showMaxThreshold(width, pointsPerPxThreshold) && isDefined(fallbackEnd)) {
plotData = filteredData;
const newEnd = getNewEnd(fallbackEnd, xAccessor, initialXScale, head(realInputDomain));
domain = [head(realInputDomain), newEnd];

const newXScale = xScale.copy().domain(domain) as
| ScaleContinuousNumeric<number, number>
| ScaleTime<number, number>;

const newWidth = Math.floor(
newXScale(xAccessor(last(plotData))) - newXScale(xAccessor(head(plotData))),
);

// @ts-ignore
log(`and ouch, that is too much, so instead showing ${plotData.length} in ${newWidth}px`);
} else {
plotData =
currentPlotData || filteredData.slice(filteredData.length - showMax(width, pointsPerPxThreshold));
domain = currentDomain || [xAccessor(head(plotData)), xAccessor(last(plotData))];

const newXScale = xScale.copy().domain(domain) as
| ScaleContinuousNumeric<number, number>
| ScaleTime<number, number>;

const newWidth = Math.floor(
newXScale(xAccessor(last(plotData))) - newXScale(xAccessor(head(plotData))),
);

// @ts-ignore
log(`and ouch, that is too much, so instead showing ${plotData.length} in ${newWidth}px`);
currentPlotData ?? filteredData.slice(filteredData.length - showMax(width, pointsPerPxThreshold));
domain = currentDomain ?? [xAccessor(head(plotData)), xAccessor(last(plotData))];
}
}
return { plotData, domain };
Expand All @@ -145,7 +114,9 @@ function extentsWrapper<TDomain extends number | Date>(
}

function canShowTheseManyPeriods(width: number, arrayLength: number, maxThreshold: number, minThreshold: number) {
return arrayLength > showMinThreshold(width, minThreshold) && arrayLength < showMaxThreshold(width, maxThreshold);
const widthAdjustedMinThreshold = showMinThreshold(width, minThreshold);
const widthAdjustedMaxTheshold = showMaxThreshold(width, maxThreshold);
return arrayLength >= widthAdjustedMinThreshold && arrayLength < widthAdjustedMaxTheshold;
}

function showMinThreshold(width: number, threshold: number) {
Expand Down
7 changes: 4 additions & 3 deletions packages/scales/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { ScaleContinuousNumeric } from "d3-scale";
import { ScaleContinuousNumeric, ScaleTime } from "d3-scale";
export {
default as discontinuousTimeScaleProvider,
discontinuousTimeScaleProviderBuilder,
} from "./discontinuousTimeScaleProvider";
export { default as financeDiscontinuousScale } from "./financeDiscontinuousScale";
export * from "./timeFormat";

export const defaultScaleProvider = (xScale: ScaleContinuousNumeric<number, number>) => {
return (data: any, xAccessor: any) => ({ data, xScale, xAccessor, displayXAccessor: xAccessor });
export const defaultScaleProvider = (xScale: ScaleContinuousNumeric<number, number> | ScaleTime<number, number>) => {
return (data: any[], xAccessor: any) => ({ data, xScale, xAccessor, displayXAccessor: xAccessor });
};
29 changes: 29 additions & 0 deletions packages/scales/src/timeFormat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { timeSecond, timeMinute, timeHour, timeDay, timeWeek, timeMonth, timeYear } from "d3-time";
import { timeFormat as d3TimeFormat } from "d3-time-format";

const formatMillisecond = d3TimeFormat(".%L");
const formatSecond = d3TimeFormat(":%S");
const formatMinute = d3TimeFormat("%H:%M");
const formatHour = d3TimeFormat("%H:%M");
const formatDay = d3TimeFormat("%e");
const formatWeek = d3TimeFormat("%e");
const formatMonth = d3TimeFormat("%b");
const formatYear = d3TimeFormat("%Y");

export const timeFormat = (date: Date) => {
return (timeSecond(date) < date
? formatMillisecond
: timeMinute(date) < date
? formatSecond
: timeHour(date) < date
? formatMinute
: timeDay(date) < date
? formatHour
: timeMonth(date) < date
? timeWeek(date) < date
? formatDay
: formatWeek
: timeYear(date) < date
? formatMonth
: formatYear)(date);
};
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class BasicLineSeries extends React.Component<ChartProps> {
ratio={ratio}
width={width}
margin={this.margin}
minPointsPerPxThreshold={0.0025}
data={data}
displayXAccessor={displayXAccessor}
seriesName="Data"
Expand Down
48 changes: 14 additions & 34 deletions packages/stories/src/features/scales/Scales.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,37 @@
import { format } from "d3-format";
import { timeSecond, timeMinute, timeHour, timeDay, timeWeek, timeMonth, timeYear } from "d3-time";
import { timeFormat } from "d3-time-format";
import { scaleTime, ScaleContinuousNumeric } from "d3-scale";
import { scaleTime, ScaleTime, ScaleContinuousNumeric } from "d3-scale";
import * as React from "react";
import { Chart, ChartCanvas, XAxis, YAxis, CandlestickSeries, withDeviceRatio, withSize } from "react-financial-charts";
import {
Chart,
ChartCanvas,
XAxis,
YAxis,
CandlestickSeries,
timeFormat,
withDeviceRatio,
withSize,
} from "react-financial-charts";
import { IOHLCData, withOHLCData } from "../../data";

const formatMillisecond = timeFormat(".%L"),
formatSecond = timeFormat(":%S"),
formatMinute = timeFormat("%H:%M"),
formatHour = timeFormat("%H:%M"),
formatDay = timeFormat("%e"),
formatWeek = timeFormat("%e"),
formatMonth = timeFormat("%b"),
formatYear = timeFormat("%Y");

interface ChartProps {
readonly data: IOHLCData[];
readonly height: number;
readonly width: number;
readonly ratio: number;
readonly xScale?: ScaleTime<number, number>;
readonly yScale?: ScaleContinuousNumeric<number, number>;
}

class Scales extends React.Component<ChartProps> {
private readonly margin = { left: 0, right: 48, top: 0, bottom: 24 };

public render() {
const { data, height, ratio, width, yScale } = this.props;
const { data, height, ratio, width, xScale = scaleTime(), yScale } = this.props;

const xAccessor = (d: IOHLCData) => d.date;
const start = xAccessor(data[data.length - 1]);
const end = xAccessor(data[Math.max(0, data.length - 100)]);
const xExtents = [start, end];
const xScale = scaleTime();

return (
<ChartCanvas
Expand All @@ -49,7 +47,7 @@ class Scales extends React.Component<ChartProps> {
>
<Chart id={1} yExtents={this.yExtents} yScale={yScale}>
<CandlestickSeries />
<XAxis tickFormat={this.timeFormat} />
<XAxis tickFormat={timeFormat} />
<YAxis tickFormat={format(".2f")} />
</Chart>
</ChartCanvas>
Expand All @@ -59,24 +57,6 @@ class Scales extends React.Component<ChartProps> {
private readonly yExtents = (data: IOHLCData) => {
return [data.high, data.low];
};

private readonly timeFormat = (date: Date) => {
return (timeSecond(date) < date
? formatMillisecond
: timeMinute(date) < date
? formatSecond
: timeHour(date) < date
? formatMinute
: timeDay(date) < date
? formatHour
: timeMonth(date) < date
? timeWeek(date) < date
? formatDay
: formatWeek
: timeYear(date) < date
? formatMonth
: formatYear)(date);
};
}

export const Daily = withOHLCData()(withSize({ style: { minHeight: 600 } })(withDeviceRatio()(Scales)));
4 changes: 3 additions & 1 deletion packages/stories/src/features/scales/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { scaleLog } from "d3-scale";
import { scaleLog, scaleUtc } from "d3-scale";
import * as React from "react";
import { Daily } from "./Scales";

Expand All @@ -8,4 +8,6 @@ export default {

export const continuousScale = () => <Daily />;

export const utcScale = () => <Daily xScale={scaleUtc()} />;

export const logScale = () => <Daily yScale={scaleLog()} />;

0 comments on commit 4b20255

Please sign in to comment.