leeoniya / uPlot

📈 A small, fast chart for time series, lines, areas, ohlc & bars
MIT License
8.69k stars 382 forks source link

When scale distr:2, scaleMin and scaleMax have wrong values #555

Open kchudoba opened 3 years ago

kchudoba commented 3 years ago

Not sure if this is a bug, but when I set distr:2 on the x scale, the scaleMin and scaleMax values that are passed to the function: space: (self, axisIdx, scaleMin, scaleMax, plotDim) are not the values of the x-data array but their indexes. So , in my case, instead of getting timestamps, I get the min and max index of the timestamp array.

I am trying to get somewhat meaningful splits/ticks with temporal data and distr:2 (for a simple stock price line chart). Do you maybe have some examples how to achieve this? The default splits (e.g. for months) don't start at the beginning of the month)

leeoniya commented 3 years ago

this is "by design" (see https://github.com/leeoniya/uPlot/issues/212). to get timestamps you should use self.data[0][min]

I am trying to get somewhat meaningful splits/ticks with temporal data and distr:2 (for a simple stock price line chart). Do you maybe have some examples how to achieve this? The default splits (e.g. for months) don't start at the beginning of the month)

uPlot natively only works with continuous scales that can be interpolated using math functions. ordinal scales are discontinuous and unpredictable, so you'll have to manually scan your data to figure out where you want the ticks. let's say there was no trading on the weekend, and that Sunday falls on the 1st of the next month, plus Monday is a national holiday also without trading, where do you put the tick for the 1st? what if you're zoomed and the 1st of the month is out of visible range, where should the 1st tick go? and what if that tick lands on a weekend? ​what about DST roll-overs? timezone offsets? that's just a few of a bunch of situations that makes discontinuous scales tricky to get "correct" from logical and performance perspectives.

if you'd like to take a stab at it with a custom axis.splits function, i'd be interested in how you handle the above questions. if all you want to do is show ticks at existing datapoints that land "somewhere" on the 1st (in your local timezone), regardless of zoom level, then it should be quite easy, but unlikely to work generically.

kchudoba commented 3 years ago

Most of the stock charts (e.g. yahoo) show the month label at the first trading day of the month (or between the last trading day of the old month and the first trading day of the new month) image

I was hoping that you (or someone) has already come up with a solution for that, as it seems a common use case. I am having a hard time trying to understand the various options described in https://github.com/leeoniya/uPlot/tree/master/docs#axis--grid-opts and axis.splits are not described there at all.

Could you maybe help to clarify the following:

I'm sorry if the questions seem basic, but I am almost sure that I'm not the only one confused with this part.

leeoniya commented 3 years ago

yes, sorry, the "docs" are in very poor shape. most of the API "docs" are simply reading the comments in the typings file:

https://github.com/leeoniya/uPlot/blob/b07903043863d2eb784f1f6fab19fa77d12f5637/dist/uPlot.d.ts#L926-L986

axis.splits() is the final function that is used to determine where to place the grid & ticks along an axis. it must return an array of scale positions. if the scale is distr: 2, then it can return integer or fractional indices, e.g. your xmin = 5 and xmax = 10, you can place a split at 5.2 and 6.5 by returning [5.2, 6.5], but then you must modify axis.values() to figure out how to render/format that fractional index split "as a date" for tick value rendering. so, in short, all you need to worry about is customizing axis.splits and axis.values and can ignore incrs or space.

you can think of distr: 2 scales as evenly-spaced-labels, with some labels being skipped when they're too dense. the algorithm for them is identical to integer (non-time) scales with a range of 0..N. it just takes every 10th or every 5th or every 2nd index from the dataset and formats the timestamp of the datapoint at that index as a date. there is zero knowledge of a calendar, or months or hours.

i looked at how https://www.tradingview.com/chart/ does this, and as you said, it will simply call the label "Nov" if it's actually Nov 2 instead of trying to place labels on Nov 1. if you want similar behavior, basically just override axis.splits() and return an array of indices where you want to put the ticks/grid/labels. once you're happy with that, then you can override axis.values() to format the tick labels at those splits to your desired format.

leeoniya commented 3 years ago

btw, feel free to PR your changes to the candlestick demo if you're happy with them, and i'll review.