leeoniya / uPlot

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

Help with the configuration #642

Closed ivanjaros closed 2 years ago

ivanjaros commented 2 years ago

I am trying to make a chart that will have visible x line as dotted line(that matches the position of the values(ticks?)), to have positive value have green color and fill and negative value to have red color and fill, both with gradient.

Something like this: obrázok

So far I have this:

{
      uplotData: [
        [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16],
        [5,-5, 0, 1,5,9,10,15,5,-10,-15,-20,-20,-5,0, 5]
      ],
      uplotOpts: {
        width: 100,
        height: 50,
        cursor: {
          show: false
        },
        select: {
          show: false,
        },
        legend: {
          show: false
        },
        scales: {
          x: {
            time: false
          }
        },
        axes: [
          {
            show: false,
          },
          {
            show: false,
          }
        ],
        series: [
          {},
          {
            stroke: "red",
            width: 1,
            fill: "rgba(255, 0, 0, 0.3)",
          }
        ],
      },

So far that will render me this: obrázok

Since the documentation is non-existent i checked the demos but I really don't get how that fill works or how to render that x- axis properly. I came from apex charts which could not render small charts without a ton of padding and chartjs documentation is horrible to go through.

leeoniya commented 2 years ago

you have to construct canvas stroke and fill gradients to get different above/below colors. take a look at https://leeoniya.github.io/uPlot/demos/gradients.html

ivanjaros commented 2 years ago

that's the(one) example i was looking at but i didn't made any progress. i don't understand that gradient function or what values to provide it to achieve the desired result(i ended up with single color no matter what..maybe it does not work with negative values?). it would be nice to have a proper documentation for a project that has almost 7k start here.

leeoniya commented 2 years ago

i don't understand that gradient function or what values to provide it to achieve the desired result

the gradient is defined as low -> high in y scale values. so you want something like

scale.min -> red 0 -> white scale.max -> green

(where the scale min/max are computed from your data range).

this should get you close:

https://jsfiddle.net/vh0o9s5n/

Code ```js let can = document.createElement("canvas"); let ctx = can.getContext("2d"); function scaleGradient(u, scaleKey, ori, scaleStops, discrete = false) { let scale = u.scales[scaleKey]; // we want the stop below or at the scaleMax // and the stop below or at the scaleMin, else the stop above scaleMin let minStopIdx; let maxStopIdx; for (let i = 0; i < scaleStops.length; i++) { let stopVal = scaleStops[i][0]; if (stopVal <= scale.min || minStopIdx == null) minStopIdx = i; maxStopIdx = i; if (stopVal >= scale.max) break; } if (minStopIdx == maxStopIdx) return scaleStops[minStopIdx][1]; let minStopVal = scaleStops[minStopIdx][0]; let maxStopVal = scaleStops[maxStopIdx][0]; if (minStopVal == -Infinity) minStopVal = scale.min; if (maxStopVal == Infinity) maxStopVal = scale.max; let minStopPos = u.valToPos(minStopVal, scaleKey, true); let maxStopPos = u.valToPos(maxStopVal, scaleKey, true); let range = minStopPos - maxStopPos; let x0, y0, x1, y1; if (ori == 1) { x0 = x1 = 0; y0 = minStopPos; y1 = maxStopPos; } else { y0 = y1 = 0; x0 = minStopPos; x1 = maxStopPos; } let grd = ctx.createLinearGradient(x0, y0, x1, y1); let prevColor; for (let i = minStopIdx; i <= maxStopIdx; i++) { let s = scaleStops[i]; let stopPos = i == minStopIdx ? minStopPos : i == maxStopIdx ? maxStopPos : u.valToPos(s[0], scaleKey, true); let pct = (minStopPos - stopPos) / range; if (discrete && i > minStopIdx) grd.addColorStop(pct, prevColor); grd.addColorStop(pct, prevColor = s[1]); } return grd; } let data = [ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], [5, -5, 0, 1, 5, 9, 10, 15, 5, -10, -15, -20, -20, -5, 0, 5] ]; let opts = { width: 100, height: 50, pxAlign: 0, drawOrder: ["series", "axes"], cursor: { show: false }, select: { show: false, }, legend: { show: false }, scales: { x: { time: false }, y: { range: (u, dataMin, dataMax) => [dataMin, dataMax], } }, axes: [ { show: false, }, { size: 0, splits: [0], ticks: { show: false, }, grid: { width: 1, stroke: "gray", dash: [3, 3], } } ], series: [ {}, { fill: (u, seriesIdx) => { let s = u.series[seriesIdx]; let sc = u.scales[s.scale]; return scaleGradient(u, s.scale, 1, [ [sc.min, "red"], [ 0, "white"], [sc.max, "green"], ]); }, stroke: (u, seriesIdx) => { let s = u.series[seriesIdx]; let sc = u.scales[s.scale]; return scaleGradient(u, s.scale, 1, [ [sc.min, "red"], [ 0, "green"], ], 1, true); }, } ], }; let u = new uPlot(opts, data, document.body); ```

image

it would be nice to have a proper documentation for a project that has almost 7k start here.

completely agree! are you offering to help write it? i'd be happy to answer any questions you have if that's the case. the gradient function itself is not a uPlot core feature, so it would be something better suited for a tutorial than docs.

leeoniya commented 2 years ago

if you dont need the fill fade, you can just do this for fill:

        return scaleGradient(u, s.scale, 1, [
          [sc.min,  "rgba(255,0,0,0.2)"],
          [     0,  "rgba(0,255,0,0.2)"],
        ], 1, true);

image

ivanjaros commented 2 years ago

Can this be also achieved with OHLC/candlestick where all candles under 0 would be red and all candles above would be green? There is the OHLC demo but it too has plenty of custom functions, so it's a thing on its own.

leeoniya commented 2 years ago

Can this be also achieved with OHLC/candlestick where all candles under 0 would be red and all candles above would be green?

you wouldnt use a gradient for this. candles have their own renderer, which would just be adjusted to draw the appropriate color for each candle. but...what you're asking for would be pretty bizzare. candles cannot be above or below zero, their movement can be positive or negative; e.g. you can easily have a candle where the top is above zero but bottom is below zero. and in general the colors in candles are understood to represent movement in that period, not absolute values of their component properties.

do you have an example image of what you're trying to acheive?

the candlestick demo is gonna get a rewrite soon (see https://github.com/leeoniya/uPlot/pull/606#issuecomment-959633192). i'll actually try to get it done today.

ivanjaros commented 2 years ago

this is the example: obrázok

It is from yahoo finance. There are candlesticks that are not OHLC but just full bars. Very thin ones, but they make up a line that is of variable girth. Something like if I would have a function for the "width" property next to fill and stroke.

leeoniya commented 2 years ago

i'm not quite understanding still. even if your vertical bar/line consists of two points like OC without the HL, then you still have to decide which endpoint is above or below zero, and the bar can span zero.

your image looks like a line chart with a fill and without variable bar width/girth. i also dont understand how you would have a solid fill with just vertical lines. do you have a more zoomed in image to demo what you mean? or some kind of other explanation for how these lines should appear. something like https://thetradingbible.com/how-to-read-hollow-candlesticks perhaps?

leeoniya commented 2 years ago

here's a yahoo finance chart

https://finance.yahoo.com/chart/AMZN/

i'm not sure how to configure it to display what you describe

image

ivanjaros commented 2 years ago

I think they are doing it like this: obrázok but each entry is so thin that it looks like continuous line that is a bit faded and edgy. The coloring is not a problem, just getting the 1px bars without any space in between.

leeoniya commented 2 years ago

how would you "fill" that to the zero line if you dont have enough data to populate every pixel with a bar? for example, if you have 10 datapoints in a chart that's 100px wide? (drawing this without a fill is a simple change to the candlestick renderer)

how would you fill something like this?

|    |
  |      |
-------------
       |
ivanjaros commented 2 years ago

the same way i did the initial chart. i take a value which will be the zero and then subtract it from each tick, therefore creating points above and under this "zero". in other words, i would manipulate the OHLC values.

leeoniya commented 2 years ago

i think we're talking past each other. let's clarify this with some code.

the chart you're showing from yahoo finance is both dense and shaded (with a gradient). the "stroke" portion in this chart (as you describe it) is just an illusion created by densely-packed vertical 1px-wide bars. okay...this is easy to simulate:

https://jsfiddle.net/6bqnhzL3/1/

image

however, your follow-up illustration does not have the gradient-shaded area between "somewhere" in each bar and the "zero" line. so, my question to you is how such shading should work? the way to shade something in Canvas is to first construct a connected polygon path to be shaded. how do we construct a polygon from a series of vertical disconnected lines?

so right now in my mind, you either get shading or you get a series of vertical lines. how to get both requires a description of how to constructed the shaded region.

ivanjaros commented 2 years ago

Just to clarify, we're talking past the solution I was after. This OHLC is just a cherry on top that I can live without.

So to answer your question about creating a polygon to use for the gradient(background, so to speak), I would take the middle value of each bar(again, there is no OHLC, only HIGH and LOW, all bars will be filled for this purpose and OPEN and CLOSE will be ignored) which would serve as value to form the line of the polygon.

obrázok

leeoniya commented 2 years ago

ok, so there are quite a few ways to approach this, depending on exactly what you want.

since you're not showing the legend, the easiest thing to do would be to simply render the bars and the shaded area as separate series. for a non-interactive sparkline type of setup you dont need to worry about the awkwardness of displaying and toggling the fill separately from the bars. this way we can re-use the line pathBuilder (which is rather complex) and benefit from uPlot's native caching.

i just added a couple opts in https://github.com/leeoniya/uPlot/commit/6d266593a0cf0533c520fc3d73861a708f0a6bee so that floating bars can be rendered using the built-in renderer instead of having to write a custom one. that commit also includes this demo: https://leeoniya.github.io/uPlot/demos/sparklines-bars.html. the top version uses a shared gradient fill for all the bars, so any which cross zero are bi-colored. the bottom one uses a disp.fill function to determine each bar's individual color.

image

ivanjaros commented 2 years ago

thanks. i will try it out when i will have some proper data to work with. will let you know how it went.

leeoniya commented 2 years ago

@ivanjaros any updates?

ivanjaros commented 2 years ago

not yet, i am having trouble with getting data to work with at the moment.