79436130

Date: 2025-02-13 12:22:12
Score: 0.5
Natty:
Report link

Take a look at this plugin I built for lightweight-charts-python ( https://github.com/EsIstJosh/lightweight-charts-python) , you can probably do some minor adjustments to get it to work using normal lightweight-charts :

import { IPrimitivePaneRenderer, Coordinate, IPrimitivePaneView, Time, ISeriesPrimitive, SeriesAttachedParameter, DataChangedScope, SeriesDataItemTypeMap, SeriesType, Logical, AutoscaleInfo, BarData, LineData, ISeriesApi, PrimitivePaneViewZOrder } from "lightweight-charts";
import { PluginBase } from "../plugin-base";
import { setOpacity } from "../helpers/colors";
import { ClosestTimeIndexFinder } from '../helpers/closest-index';
import { hasColorOption } from "../helpers/typeguards";
export class FillArea extends PluginBase implements ISeriesPrimitive<Time> {
    static type = "Fill Area"; // Explicitly set the type name
    
    _paneViews: FillAreaPaneView[];
    _originSeries: ISeriesApi<SeriesType>;
    _destinationSeries: ISeriesApi<SeriesType>;
    _bandsData: BandData[] = [];
    options: Required<FillAreaOptions>;
    _timeIndices: ClosestTimeIndexFinder<{ time: number }>;

    constructor(
        originSeries: ISeriesApi<SeriesType>,
        destinationSeries: ISeriesApi<SeriesType>,
        options: FillAreaOptions
    ) {
        super();
    
        // Existing logic for setting colors
        const defaultOriginColor = setOpacity('#0000FF', 0.25); // Blue
        const defaultDestinationColor = setOpacity('#FF0000', 0.25); // Red
        const originSeriesColor = hasColorOption(originSeries)
            ? setOpacity((originSeries.options() as any).lineColor || defaultOriginColor, 0.3)
            : setOpacity(defaultOriginColor, 0.3);
        const destinationSeriesColor = hasColorOption(destinationSeries)
            ? setOpacity((destinationSeries.options() as any).lineColor || defaultDestinationColor, 0.3)
            : setOpacity(defaultDestinationColor, 0.3);
    
        this.options = {
            ...defaultFillAreaOptions,
            ...options,
            originColor: options.originColor ?? originSeriesColor,
            destinationColor: options.destinationColor ?? destinationSeriesColor,
        };
    
        this._paneViews = [new FillAreaPaneView(this)];
        this._timeIndices = new ClosestTimeIndexFinder([]);
        this._originSeries = originSeries;
        this._destinationSeries = destinationSeries;
    
        // Subscribe to data changes in both series
        this._originSeries.subscribeDataChanged(() => {
            console.log("Origin series data has changed. Recalculating bands.");
            this.dataUpdated('full');
            this.updateAllViews();
        });
    
        this._destinationSeries.subscribeDataChanged(() => {
            console.log("Destination series data has changed. Recalculating bands.");
            this.dataUpdated('full');
            this.updateAllViews();
        });
    }
    
    

    updateAllViews() {
        this._paneViews.forEach(pw => pw.update());
    }
    applyOptions(options: Partial<FillAreaOptions>) {

        this.options = {
            ...this.options,
            ...options,
        };
    
        this.calculateBands();
        this.updateAllViews();
        super.requestUpdate();
    
        console.log("FillArea options updated:", this.options);
    }
    
    
    
    paneViews() {
        return this._paneViews;
    }

    attached(p: SeriesAttachedParameter<Time>): void {
        super.attached(p);
        this.dataUpdated('full');
    }

    dataUpdated(scope: DataChangedScope) {
        this.calculateBands();
        if (scope === 'full') {
            const originData = this._originSeries.data();
            this._timeIndices = new ClosestTimeIndexFinder(
                [...originData]  as { time: number }[]
            );
        }
    }

    calculateBands() {
        const originData = this._originSeries.data();
        const destinationData = this._destinationSeries.data();

        // Ensure both datasets have the same length
        const alignedData = this._alignDataLengths([...originData], [...destinationData]);

        const bandData: BandData[] = [];
        for (let i = 0; i < alignedData.origin.length; i++) {
            let points = extractPrices(alignedData.origin[i],alignedData.destination[i]);

            if (points?.originValue === undefined || points?.destinationValue === undefined) continue;

            // Determine which series is upper and lower
            const upper = Math.max(points?.originValue, points?.destinationValue);
            const lower = Math.min(points?.originValue, points?.destinationValue);

            bandData.push({
                time: alignedData.origin[i].time,
                origin: points?.originValue,
                destination: points?.destinationValue,
                upper,
                lower,
            });
        }

        this._bandsData = bandData;
    }

    _alignDataLengths(
        originData: SeriesDataItemTypeMap[SeriesType][],
        destinationData: SeriesDataItemTypeMap[SeriesType][]
    ): { origin: SeriesDataItemTypeMap[SeriesType][], destination: SeriesDataItemTypeMap[SeriesType][] } {
        const originLength = originData.length;
        const destinationLength = destinationData.length;

        if (originLength > destinationLength) {
            const lastKnown = destinationData[destinationLength - 1];
            while (destinationData.length < originLength) {
                destinationData.push({ ...lastKnown });
            }
        } else if (destinationLength > originLength) {
            const lastKnown = originData[originLength - 1];
            while (originData.length < destinationLength) {
                originData.push({ ...lastKnown });
            }
        }

        return { origin: originData, destination: destinationData };
    }

    autoscaleInfo(startTimePoint: Logical, endTimePoint: Logical): AutoscaleInfo {
        const ts = this.chart.timeScale();
        const startTime = (ts.coordinateToTime(
            ts.logicalToCoordinate(startTimePoint) ?? 0
        ) ?? 0) as number;
        const endTime = (ts.coordinateToTime(
            ts.logicalToCoordinate(endTimePoint) ?? 5000000000
        ) ?? 5000000000) as number;
        const startIndex = this._timeIndices.findClosestIndex(startTime, 'left');
        const endIndex = this._timeIndices.findClosestIndex(endTime, 'right');

        const range = {
            minValue: Math.min(...this._bandsData.map(b => b.lower).slice(startIndex, endIndex + 1)),
            maxValue: Math.max(...this._bandsData.map(b => b.upper).slice(startIndex, endIndex + 1)),
        };

        return {
            priceRange: {
                minValue: range.minValue,
                maxValue: range.maxValue,
            },
        };
    }
}
class FillAreaPaneRenderer implements IPrimitivePaneRenderer {
    _viewData: BandViewData;
    _options: FillAreaOptions;

    constructor(data: BandViewData) {
        this._viewData = data;
        this._options = data.options;
    }

    draw() {}
    drawBackground(target: CanvasRenderingTarget2D) {
        const points: BandRendererData[] = this._viewData.data;
        const options = this._options;

        if (points.length < 2) return; // Ensure there are enough points to draw

        target.useBitmapCoordinateSpace((scope) => {
            const ctx = scope.context;
            ctx.scale(scope.horizontalPixelRatio, scope.verticalPixelRatio);

            let currentPathStarted = false;
            let startIndex = 0;

            for (let i = 0; i < points.length - 1; i++) {
                const current = points[i];
                const next = points[i + 1];

                if (!currentPathStarted || current.isOriginAbove !== points[i - 1]?.isOriginAbove) {
                    if (currentPathStarted) {
                        for (let j = i - 1; j >= startIndex; j--) {
                            ctx.lineTo(points[j].x, points[j].destination);
                        }
                        ctx.closePath();
                        ctx.fill();
                    }

                    ctx.beginPath();
                    ctx.moveTo(current.x, current.origin);

                    ctx.fillStyle = current.isOriginAbove
                        ? options.originColor || 'rgba(0, 0, 0, 0)' // Default to transparent if null
                        : options.destinationColor || 'rgba(0, 0, 0, 0)'; // Default to transparent if null

                    startIndex = i;
                    currentPathStarted = true;
                }

                ctx.lineTo(next.x, next.origin);

                if (i === points.length - 2 || next.isOriginAbove !== current.isOriginAbove) {
                    for (let j = i + 1; j >= startIndex; j--) {
                        ctx.lineTo(points[j].x, points[j].destination);
                    }
                    ctx.closePath();
                    ctx.fill();
                    currentPathStarted = false;
                }
            }

            if (options.lineWidth) {
                ctx.lineWidth = options.lineWidth;
                ctx.strokeStyle = options.originColor || 'rgba(0, 0, 0, 0)';
                ctx.stroke();
            }
        });
    }
}

class FillAreaPaneView implements IPrimitivePaneView {
    _source: FillArea;
    _data: BandViewData;

    constructor(source: FillArea) {
        this._source = source;
        this._data = {
            data: [],
            options: this._source.options, // Pass the options for the renderer
        };
    }

    update() {
        const timeScale = this._source.chart.timeScale();

        this._data.data = this._source._bandsData.map((d) => ({
            x: timeScale.timeToCoordinate(d.time)!,
            origin: this._source._originSeries.priceToCoordinate(d.origin)!,
            destination: this._source._destinationSeries.priceToCoordinate(d.destination)!,
            isOriginAbove: d.origin > d.destination,
        }));

        // Ensure options are updated in the data
        this._data.options = this._source.options;
    }

    renderer() {
        return new FillAreaPaneRenderer(this._data);
    }
    zOrder() {
        return 'bottom' as PrimitivePaneViewZOrder;
    }
}



export interface FillAreaOptions {
    originColor: string | null; // Color for origin on top
    destinationColor: string | null; 
    lineWidth: number | null;
};

export const defaultFillAreaOptions: Required<FillAreaOptions> = {
    originColor: null,
    destinationColor: null,
    lineWidth: null,
};

interface BandData {
    time: Time;
    origin: number; // Price value from the origin series
    destination: number; // Price value from the destination series
    upper: number; // The upper value for rendering
    lower: number; // The lower value for rendering
};
interface BandViewData {
    data: BandRendererData[];
    options: Required<FillAreaOptions>;
};
interface BandRendererData {
    x: Coordinate | number;
    origin: Coordinate | number;
    destination: Coordinate | number;
    isOriginAbove: boolean; // True if the origin series is above the destination series
}

function extractPrices(
    originPoint: SeriesDataItemTypeMap[SeriesType],
    destinationPoint: SeriesDataItemTypeMap[SeriesType]
): {originValue: number| undefined, destinationValue: number| undefined} | undefined {
    let originPrice: number | undefined;
    let destinationPrice: number | undefined;

    // Extract origin price
    if ((originPoint as BarData).close !== undefined) {
        const originBar = originPoint as BarData;
        originPrice = originBar.close; // Use close price for comparison
    } else if ((originPoint as LineData).value !== undefined) {
        originPrice = (originPoint as LineData).value; // Use value for LineData
    }

    // Extract destination price
    if ((destinationPoint as BarData).close !== undefined) {
        const destinationBar = destinationPoint as BarData;
        destinationPrice = destinationBar.close; // Use close price for comparison
    } else if ((destinationPoint as LineData).value !== undefined) {
        destinationPrice = (destinationPoint as LineData).value; // Use value for LineData
    }

    // Ensure both prices are defined
    if (originPrice === undefined || destinationPrice === undefined) {
        return undefined;
    }

    // Handle mixed types and determine the appropriate values to return
    if (originPrice < destinationPrice) {
        // origin > destination: min(open, close) for BarData (if applicable), otherwise value
        const originValue =
            (originPoint as BarData).close !== undefined
                ? Math.min((originPoint as BarData).open, (originPoint as BarData).close)
                : originPrice;

        const destinationValue =
            (destinationPoint as BarData).close !== undefined
                ? Math.max((destinationPoint as BarData).open, (destinationPoint as BarData).close)
                : destinationPrice;

        return {originValue, destinationValue};
    } else {
        // origin <= destination: max(open, close) for BarData (if applicable), otherwise value
        const originValue =
            (originPoint as BarData).close !== undefined
                ? Math.max((originPoint as BarData).open, (originPoint as BarData).close)
                : originPrice;

        const destinationValue =
            (destinationPoint as BarData).close !== undefined
                ? Math.min((destinationPoint as BarData).open, (destinationPoint as BarData).close)
                : destinationPrice;

        return {originValue, destinationValue};
    }
}```
Reasons:
  • Blacklisted phrase (1): this plugin
  • Long answer (-1):
  • Has code block (-0.5):
  • Low reputation (1):
Posted by: Turnt