@indiminds/charts 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +14 -0
- package/README.md +159 -0
- package/dist/bar-chart/bar-chart.d.ts +26 -0
- package/dist/bar-chart/index.d.ts +2 -0
- package/dist/bar-chart/index.js +1 -0
- package/dist/bar-chart/types.d.ts +48 -0
- package/dist/candlestick/candlestick-chart.d.ts +73 -0
- package/dist/candlestick/data-sync.d.ts +29 -0
- package/dist/candlestick/index.d.ts +2 -0
- package/dist/candlestick/index.js +1 -0
- package/dist/candlestick/interaction.d.ts +10 -0
- package/dist/candlestick/layout-calculator.d.ts +23 -0
- package/dist/candlestick/pane-state.d.ts +19 -0
- package/dist/candlestick/panel-resize.d.ts +4 -0
- package/dist/candlestick/price-style.d.ts +9 -0
- package/dist/candlestick/renderers/candles.d.ts +5 -0
- package/dist/candlestick/renderers/grid.d.ts +2 -0
- package/dist/candlestick/renderers/index.d.ts +20 -0
- package/dist/candlestick/renderers/indicators.d.ts +30 -0
- package/dist/candlestick/renderers/overlays.d.ts +8 -0
- package/dist/candlestick/renderers/price-line.d.ts +4 -0
- package/dist/candlestick/viewport.d.ts +28 -0
- package/dist/canvas-chart-base.d.ts +23 -0
- package/dist/category-axis.d.ts +9 -0
- package/dist/category-layout.d.ts +9 -0
- package/dist/chart-shell.d.ts +14 -0
- package/dist/combo-chart/combo-chart.d.ts +26 -0
- package/dist/combo-chart/index.d.ts +2 -0
- package/dist/combo-chart/index.js +1 -0
- package/dist/combo-chart/types.d.ts +75 -0
- package/dist/config.d.ts +94 -0
- package/dist/line-chart/index.d.ts +2 -0
- package/dist/line-chart/index.js +1 -0
- package/dist/line-chart/line-chart.d.ts +21 -0
- package/dist/line-chart/types.d.ts +30 -0
- package/dist/pie-chart/index.d.ts +2 -0
- package/dist/pie-chart/index.js +1 -0
- package/dist/pie-chart/pie-chart.d.ts +21 -0
- package/dist/pie-chart/types.d.ts +26 -0
- package/dist/plot-frame.d.ts +19 -0
- package/dist/renderers/helpers.d.ts +17 -0
- package/dist/renderers/primitives/axis-pills.d.ts +11 -0
- package/dist/renderers/primitives/context.d.ts +36 -0
- package/dist/renderers/primitives/decorations.d.ts +44 -0
- package/dist/renderers/primitives/grouped-bars.d.ts +43 -0
- package/dist/renderers/primitives/index.d.ts +5 -0
- package/dist/renderers/primitives/marks.d.ts +40 -0
- package/dist/renderers/value-axis.d.ts +18 -0
- package/dist/scale.d.ts +36 -0
- package/dist/types.d.ts +53 -0
- package/package.json +67 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Which slice of the candles is on screen, and how zooming and panning move
|
|
2
|
+
* it. The chart only calls methods here — it never touches the offsets. */
|
|
3
|
+
export declare class ViewportState {
|
|
4
|
+
visibleCandles: number;
|
|
5
|
+
/** How far the view sits from the newest candle, in candles. Negative means
|
|
6
|
+
* empty space on the right, which is the normal resting position. */
|
|
7
|
+
rightOffset: number;
|
|
8
|
+
private dataLength;
|
|
9
|
+
private chartWidth;
|
|
10
|
+
/** The chart re-tells us these on every resize and data update. */
|
|
11
|
+
update(dataLength: number, chartWidth: number): void;
|
|
12
|
+
get candleWidth(): number;
|
|
13
|
+
get rightBound(): number;
|
|
14
|
+
get leftBound(): number;
|
|
15
|
+
get minRightOffset(): number;
|
|
16
|
+
get defaultRightOffset(): number;
|
|
17
|
+
/** Back to the default view — first load, or a new symbol. */
|
|
18
|
+
reset(dataLength: number): void;
|
|
19
|
+
/** Zoom around a specific X position on the canvas. */
|
|
20
|
+
zoom(deltaY: number, mouseX: number): void;
|
|
21
|
+
/** Shift the view sideways by a pixel amount. */
|
|
22
|
+
pan(deltaX: number): void;
|
|
23
|
+
/** A new candle arrived: step along with it so the view doesn't drift, but
|
|
24
|
+
* only if the user has already scrolled back from the live edge. */
|
|
25
|
+
onLiveTick(candlesAdded: number): void;
|
|
26
|
+
private clampMin;
|
|
27
|
+
private clampMax;
|
|
28
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ChartShell } from "./chart-shell";
|
|
2
|
+
export declare abstract class CanvasChartEngine<THoverEvent> {
|
|
3
|
+
protected readonly canvas: HTMLCanvasElement;
|
|
4
|
+
protected readonly shell: ChartShell;
|
|
5
|
+
protected width: number;
|
|
6
|
+
protected height: number;
|
|
7
|
+
protected mouseX: number | null;
|
|
8
|
+
protected mouseY: number | null;
|
|
9
|
+
private animationFrame;
|
|
10
|
+
/** Fires on every hover change, including leaving the canvas (null). */
|
|
11
|
+
onHoverChange?: (event: THoverEvent | null) => void;
|
|
12
|
+
constructor(canvas: HTMLCanvasElement, container: HTMLElement);
|
|
13
|
+
/** Redraws everything. Subclasses read the size and mouse fields above. */
|
|
14
|
+
protected abstract render(): void;
|
|
15
|
+
destroy(): void;
|
|
16
|
+
/** Runs a 0-to-1 animation that slows as it finishes, cancelling any still
|
|
17
|
+
* going. A higher `power` makes it snappier. The last frame lands on 1. */
|
|
18
|
+
protected animate(durationMs: number, power: number, onFrame: (eased: number) => void): void;
|
|
19
|
+
private cancelAnimation;
|
|
20
|
+
private resize;
|
|
21
|
+
private handleMouseMove;
|
|
22
|
+
private handleMouseLeave;
|
|
23
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DrawContext } from "./renderers/primitives/context";
|
|
2
|
+
export declare function drawCategoryAxis(c: DrawContext, categories: readonly string[], axisTop: number): void;
|
|
3
|
+
/** How many categories to skip between drawn labels so neighbours don't
|
|
4
|
+
* collide. The same step spaces the vertical gridlines, so lines and labels
|
|
5
|
+
* always land on the same ticks. */
|
|
6
|
+
export declare function measureLabelStep(ctx: CanvasRenderingContext2D, categories: readonly string[], toX: (i: number) => number): number;
|
|
7
|
+
/** Labels only, every `step`th category — for charts that draw their own
|
|
8
|
+
* gridlines (the dual-axis ones) rather than using drawCategoryAxis. */
|
|
9
|
+
export declare function drawCategoryLabels(ctx: CanvasRenderingContext2D, categories: readonly string[], toX: (i: number) => number, axisTop: number, step: number): void;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface CategoryLayout {
|
|
2
|
+
slotWidth: number;
|
|
3
|
+
/** Category index -> canvas X, slot center. Same shape as DrawContext.toX. */
|
|
4
|
+
toX: (categoryIndex: number) => number;
|
|
5
|
+
}
|
|
6
|
+
export declare function computeCategoryLayout(categoryCount: number, availableWidth: number): CategoryLayout;
|
|
7
|
+
/** Category whose slot center sits closest to `x` — how hover snaps to a
|
|
8
|
+
* category on charts whose plot area starts past a left gutter. */
|
|
9
|
+
export declare function nearestCategoryIndex(x: number, categoryCount: number, toX: (categoryIndex: number) => number): number | null;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ChartShellCallbacks {
|
|
2
|
+
/** The container changed size. Already deferred to the next frame. */
|
|
3
|
+
onResize: (width: number, height: number) => void;
|
|
4
|
+
/** The theme changed. COLORS has already been updated by the time this runs. */
|
|
5
|
+
onThemeChange: () => void;
|
|
6
|
+
}
|
|
7
|
+
export interface ChartShell {
|
|
8
|
+
ctx: CanvasRenderingContext2D;
|
|
9
|
+
/** Resizes the canvas for the current screen, so it stays sharp on a
|
|
10
|
+
* high-density display. Call once per resize. */
|
|
11
|
+
resizeBackingStore: (width: number, height: number) => void;
|
|
12
|
+
destroy: () => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function createChartShell(canvas: HTMLCanvasElement, container: HTMLElement, callbacks: ChartShellCallbacks): ChartShell;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CanvasChartEngine } from "../canvas-chart-base";
|
|
2
|
+
import type { ComboChartData, ComboHoverEvent } from "./types";
|
|
3
|
+
/** Grouped bars with lines drawn over them, sharing one category axis. Its own
|
|
4
|
+
* class rather than a mode on the bar or line chart — it's a different shape. */
|
|
5
|
+
export declare class ComboChart extends CanvasChartEngine<ComboHoverEvent> {
|
|
6
|
+
private data;
|
|
7
|
+
/** 0-1 sweep applied to the bars/lines only — 1 means fully drawn. */
|
|
8
|
+
private reveal;
|
|
9
|
+
/** Swaps the data and replays it in: the bars and overlay lines sweep in
|
|
10
|
+
* left to right while the axes/labels stay put. Runs on every swap — a
|
|
11
|
+
* whole new category set is exactly when the redraw is most abrupt. */
|
|
12
|
+
setData(data: ComboChartData): void;
|
|
13
|
+
protected render(): void;
|
|
14
|
+
/** Tinted spans behind the whole plot, plus a solid ribbon in the top
|
|
15
|
+
* gutter so the boundaries stay legible at a tint you can barely see.
|
|
16
|
+
* Drawn first so nothing sits on top of the bars. */
|
|
17
|
+
private drawBands;
|
|
18
|
+
/** Rotated axis title, centered alongside the tick labels — the same idea as
|
|
19
|
+
* any chart's axis name, just turned on its side. */
|
|
20
|
+
private drawAxisTitles;
|
|
21
|
+
/** Pill callouts above a category's bars, pre-formatted by the caller. */
|
|
22
|
+
private drawMarkers;
|
|
23
|
+
/** Crosshair plus the hovered category's values — the tooltip itself is a
|
|
24
|
+
* DOM overlay owned by whoever consumes onHoverChange. */
|
|
25
|
+
private drawHover;
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var Ce=Object.defineProperty;var ye=(e,n,t)=>n in e?Ce(e,n,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[n]=t;var x=(e,n,t)=>ye(e,typeof n!="symbol"?n+"":n,t);var M={bullish:"#22c55e",bearish:"#ef4444",grid:"#313150",text:"#8b8b93",crosshair:"#a1a1aa",priceLine:"#3b82f6",pill:"#313150",pillText:"#ffffff",divider:"#505064",dividerActive:"#8c8cb9",background:"transparent"},Me=[["--chart-grid","grid"],["--chart-axis","text"],["--chart-crosshair","crosshair"],["--chart-1","priceLine"],["--chart-pill","pill"],["--chart-pill-foreground","pillText"],["--chart-divider","divider"],["--chart-divider-active","dividerActive"]];function X(){if(typeof document>"u")return;let e=getComputedStyle(document.documentElement);for(let[n,t]of Me){let o=e.getPropertyValue(n).trim();o&&(M[t]=o)}}function Z(e,n,t){let o=e.getContext("2d");if(!o)throw new Error("Could not get 2D context");let r=new ResizeObserver(l=>{if(l.length===0)return;let{width:s,height:b}=l[0].contentRect;requestAnimationFrame(()=>t.onResize(Math.floor(s),Math.floor(b)))});r.observe(n),X();let i=new MutationObserver(()=>{X(),t.onThemeChange()});i.observe(document.documentElement,{attributes:!0,attributeFilter:["class"]});function a(l,s){let b=window.devicePixelRatio||1;o.setTransform(1,0,0,1,0,0),e.width=Math.floor(l*b),e.height=Math.floor(s*b),e.style.width=`${l}px`,e.style.height=`${s}px`,o.scale(b,b)}return{ctx:o,resizeBackingStore:a,destroy:()=>{r.disconnect(),i.disconnect()}}}var O=class{constructor(n,t){x(this,"canvas");x(this,"shell");x(this,"width",0);x(this,"height",0);x(this,"mouseX",null);x(this,"mouseY",null);x(this,"animationFrame",null);x(this,"onHoverChange");x(this,"handleMouseMove",n=>{let t=this.canvas.getBoundingClientRect();this.mouseX=n.clientX-t.left,this.mouseY=n.clientY-t.top,this.render()});x(this,"handleMouseLeave",()=>{this.mouseX=null,this.mouseY=null,this.onHoverChange?.(null),this.render()});this.canvas=n,this.shell=Z(n,t,{onResize:(o,r)=>this.resize(o,r),onThemeChange:()=>this.render()}),n.addEventListener("mousemove",this.handleMouseMove),n.addEventListener("mouseleave",this.handleMouseLeave)}destroy(){this.cancelAnimation(),this.canvas.removeEventListener("mousemove",this.handleMouseMove),this.canvas.removeEventListener("mouseleave",this.handleMouseLeave),this.shell.destroy()}animate(n,t,o){this.cancelAnimation();let r=performance.now(),i=a=>{let l=Math.min(1,(a-r)/n);o(1-(1-l)**t),this.render(),this.animationFrame=l<1?requestAnimationFrame(i):null};this.animationFrame=requestAnimationFrame(i)}cancelAnimation(){this.animationFrame!=null&&(cancelAnimationFrame(this.animationFrame),this.animationFrame=null)}resize(n,t){n===0||t===0||(this.width=n,this.height=t,this.shell.resizeBackingStore(n,t),this.render())}};var S="11px Inter, sans-serif";function I(e,n){let{ctx:t}=e;t.save(),t.beginPath(),t.rect(0,e.top,e.chartWidth,e.height),t.clip(),t.beginPath(),n(t),t.restore()}function V(e,n){e.strokeStyle=n.color,e.fillStyle=n.color,e.lineWidth=n.lineWidth??1.5,e.globalAlpha=n.opacity??1,e.setLineDash(n.dash??[])}function j(e,n,t){return Math.max(n/2,Math.min(e,t-n/2))}function Q(e,n,t,o){e.save(),e.strokeStyle=M.crosshair,e.lineWidth=1,e.setLineDash([4,4]),e.beginPath(),e.moveTo(n,t),e.lineTo(n,o),e.stroke(),e.restore()}function ee(e,n){let t=e/Math.max(1,n),o=Math.pow(10,Math.floor(Math.log10(Math.max(t,1e-10)))),r=t/o,i;return r<1.5?i=1:r<3.5?i=2:r<7.5?i=5:i=10,i*o}function te(e,n,t,o,r,i={}){let{paddingX:a=6,height:l=16,radius:s=4}=i;e.font=r.font??S,e.textBaseline="middle",e.textAlign="center";let b=e.measureText(o).width+a*2;return e.fillStyle=r.background,Se(e,n-b/2,t,b,l,s),e.fill(),e.fillStyle=r.text,e.fillText(o,n,t+l/2),b}function Se(e,n,t,o,r,i){e.beginPath(),e.moveTo(n+i,t),e.arcTo(n+o,t,n+o,t+r,i),e.arcTo(n+o,t+r,n,t+r,i),e.arcTo(n,t+r,n,t,i),e.arcTo(n,t,n+o,t,i),e.closePath()}function ne(e,n,t,o){if(e.mode==="price")return null;let r=1/0,i=-1/0;for(let m of n){let h=Math.min(o,m.length);for(let f=Math.max(0,t);f<h;f++){let c=m[f];c==null||!Number.isFinite(c)||(c<r&&(r=c),c>i&&(i=c))}}let a=Number.isFinite(r)&&Number.isFinite(i);if(e.mode==="auto"){if(!a)return null;let m=Math.max(i-r,1e-10),h=Math.max(m*e.cushionRatio,e.minCushion??0);return{min:r-h,max:i+h}}if(!a){let[m,h]=e.alwaysInclude??[e.min,e.max];return{min:m,max:h}}let l=i-r,s=Math.max(l*e.cushionRatio,e.minCushion??0),b=r-s,u=i+s;return e.alwaysInclude&&(b=Math.min(b,e.alwaysInclude[0]),u=Math.max(u,e.alwaysInclude[1])),{min:Math.max(b,e.min),max:Math.min(u,e.max)}}function F(e,n,t,o=8,r=8){let i=Math.max(1,t-o-r),a=Math.max(e.max-e.min,1e-10);return l=>n+o+(e.max-l)/a*i}function oe(e,n,t,o){let r=Math.max(1,o);return{left:e,right:n,top:t,width:Math.max(1,n-e),height:r,bottom:t+r}}function k(e){return Math.round(e)+.5}function B(e,n,t){return ne({mode:"auto",cushionRatio:t},e,0,n)??{min:0,max:1}}function re(e,n){let t=ee(e.max-e.min,Math.max(2,Math.round(n/45))),o=[];for(let r=Math.ceil(e.min/t)*t;r<=e.max;r+=t)o.push(r);return o}function Te(e,n,t,o){let r=e.candleWidth*t,i=r/n,a=Math.max(1,i*(1-o));return{groupWidth:r,slotWidth:i,barWidth:a}}function Pe(e,n,t){return e-n.groupWidth/2+t*n.slotWidth+(n.slotWidth-n.barWidth)/2}var ie=new Map;function we(e,n){let t=ie.get(n);if(t)return t;let o=document.createElement("canvas");o.width=6,o.height=6;let r=o.getContext("2d");if(!r)return n;r.strokeStyle=n,r.lineWidth=1.5,r.beginPath(),r.moveTo(0,6),r.lineTo(6,0),r.moveTo(-1.5,1.5),r.lineTo(1.5,-1.5),r.moveTo(4.5,7.5),r.lineTo(7.5,4.5),r.stroke();let i=e.createPattern(o,"repeat");return i?(ie.set(n,i),i):n}var De=.45;function Re(e,n){if(!/^#[0-9a-fA-F]{6}$/.test(e))return e;let t=parseInt(e.slice(1,3),16),o=parseInt(e.slice(3,5),16),r=parseInt(e.slice(5,7),16),i=a=>Math.round(a+(255-a)*n);return`rgb(${i(t)}, ${i(o)}, ${i(r)})`}function G(e,n,t={}){if(n.length===0)return;let{baseline:o=0,groupWidthRatio:r=.7,barGapRatio:i=.15,focusIndex:a=null,growProgress:l=1,slotCount:s,slotOffset:b=0}=t;I(e,u=>{u.globalAlpha=1,u.setLineDash([]);let m=e.toY(o),h=Te(e,s??n.length,r,i),f=Math.min(e.endIndex,...n.map(c=>c.values.length));for(let c=Math.max(0,e.startIndex);c<f;c++){let v=e.toX(c),g=a!=null&&c===a;n.forEach((C,d)=>{let T=C.values[c];if(T==null||!Number.isFinite(T))return;let y=C.fillModes?.[c]??"solid",E=e.toY(T),P=Pe(v,h,b+d),w=Math.max(1,Math.abs(E-m)*l),D=E<=m?m-w:m,W=C.colors?.[c]??C.color,R=g?Re(W,De):W;if(y==="outline"){u.strokeStyle=R,u.lineWidth=g?2:1.5,u.strokeRect(P+.75,D+.75,Math.max(0,h.barWidth-1.5),Math.max(0,w-1.5));return}u.fillStyle=y==="hatched"?we(u,R):R,u.fillRect(P,D,h.barWidth,w),y==="hatched"&&(u.strokeStyle=R,u.lineWidth=1,u.strokeRect(P+.5,D+.5,Math.max(0,h.barWidth-1),Math.max(0,w-1))),g&&(u.strokeStyle=R,u.lineWidth=1.5,u.strokeRect(P+.75,D+.75,Math.max(0,h.barWidth-1.5),Math.max(0,w-1.5)))})}})}function ae(e,n,t,o={}){let{colorMode:r="fixed",momentumColors:i,colorValues:a}=o;I(e,l=>{V(l,t),l.lineJoin="round",l.lineCap="round";let s=r==="directional"&&!!i,b=Math.min(e.endIndex,n.length),u=null,m=null,h=null;for(let f=Math.max(0,e.startIndex);f<b;f++){let c=n[f];if(c==null||!Number.isFinite(c)){u=null,m=null,h=null;continue}let v=e.toX(f),g=e.toY(c),C=a?.[f],d=C!=null&&C<0,T=s&&h!==null&&d!==h;if(u==null||m==null||T){s||l.moveTo(v,g),u=v,m=g,h=d;continue}s?(l.strokeStyle=d?i.bearStrong:i.bullStrong,l.beginPath(),l.moveTo(u,m),l.lineTo(v,g),l.stroke()):l.lineTo(v,g),u=v,m=g,h=d}s||l.stroke()})}function $(e,n,t){let o=t.side==="left";e.save(),e.font=S,e.lineWidth=1,e.textBaseline="middle",e.textAlign=o?"right":"left";let r=o?n.left-8:n.right+8;for(let i of re(t.extent,n.height)){let a=t.toY(i);a<n.top||a>n.bottom||(t.gridlines&&(e.strokeStyle=M.grid,e.beginPath(),e.moveTo(n.left,k(a)),e.lineTo(n.right,k(a)),e.stroke()),e.fillStyle=M.text,e.fillText(t.format(i),r,a))}e.restore()}function le(e,n){e.save(),e.strokeStyle=M.grid,e.lineWidth=1,e.beginPath(),e.moveTo(n.left,k(n.bottom)),e.lineTo(n.right,k(n.bottom)),e.stroke(),e.restore()}function se(e,n,t){e.save(),e.font=S;let o=24;for(let i of n)o=Math.max(o,e.measureText(i).width);e.restore();let r=n.length>=2?t(1)-t(0):o+12;return Math.max(1,Math.ceil((o+16)/Math.max(1,r)))}function ue(e,n,t,o,r){e.save(),e.font=S,e.textBaseline="top",e.textAlign="center",e.fillStyle=M.text;for(let i=0;i<n.length;i+=r)e.fillText(n[i],t(i),o+8);e.restore()}function me(e,n){let t=e>0?n/e:n;return{slotWidth:t,toX:o=>o*t+t/2}}function he(e,n,t){if(n===0)return null;let o=0,r=1/0;for(let i=0;i<n;i++){let a=Math.abs(t(i)-e);a<r&&(r=a,o=i)}return o}var Ae=56,de=46,Ee=30,be=22,We=3,ce=9,Ie=.32,A=0,Y=16,Fe=700,z=class extends O{constructor(){super(...arguments);x(this,"data",{categories:[],bars:[]});x(this,"reveal",1)}setData(t){this.data=t,this.reveal=0,this.animate(Fe,2,o=>{this.reveal=o})}render(){let{ctx:t}=this.shell;if(this.width===0||this.height===0)return;t.clearRect(0,0,this.width,this.height);let{categories:o,bars:r,lineOverlays:i=[],markers:a=[],bands:l=[],subBars:s,leftFormatter:b=p=>p.toLocaleString(),leftAxisLabel:u,rightAxisLabel:m}=this.data;if(o.length===0)return;let h=o.length,f=i.filter(p=>p.axis==="right"),c=i.filter(p=>p.axis==="auto"),v=f.length>0,g=s?ke(s.values,h):0,C=g>0,d=oe(Ae+(u?Y:0),this.width-(v?de+(m?Y:0):8),be,this.height-be-Ee),T=me(h,d.width),y=p=>d.left+T.toX(p),E=B(r.map(p=>p.values),h,.1),P={min:Math.min(E.min,A),max:Math.max(E.max,A)},w=F(P,d.top,d.height,0,0),D=v?B(f.map(p=>p.values),h,.1):{min:0,max:1},W=F(D,d.top,d.height,0,0),R=d.height*Ie,U={min:0,max:g>0?g:1},H=C?F(U,d.bottom-R,R,0,0):null,K=new Map;for(let p of c){let L=B([p.values],h,.15);K.set(p.id,F(L,d.top,d.height,0,0))}this.drawBands(t,d,y,T.slotWidth,l,h),$(t,d,{extent:P,toY:w,format:b,side:"left",gridlines:!0}),v&&$(t,d,{extent:D,toY:W,format:f[0]?.formatter??(p=>p.toFixed(2)),side:"right",gridlines:!1}),le(t,d),this.drawAxisTitles(t,d,u,v?m:void 0);let ge=se(t,o,y);ue(t,o,y,d.bottom,ge);let N={ctx:t,chartWidth:d.right,priceScaleWidth:de,top:d.top,height:d.height,extent:P,candleWidth:T.slotWidth,startIndex:0,endIndex:h,toX:y,toY:w},_=this.mouseX==null?null:he(this.mouseX,h,y),J=r.length+(H?1:0);G(N,r,{baseline:A,focusIndex:_,growProgress:this.reveal,slotCount:J}),s&&H&&G({...N,extent:U,toY:H},[{color:s.color,values:s.values}],{baseline:A,focusIndex:_,growProgress:this.reveal,slotCount:J,slotOffset:r.length});let ve=Math.max(1,Math.ceil(h*this.reveal));for(let p of[...f,...c]){let L=p.axis==="right",xe={...N,extent:L?D:P,toY:L?W:K.get(p.id),endIndex:ve},q=p.style==="dashed";ae(xe,p.values,{color:p.color,lineWidth:q?1.5:2.5,dash:q?[5,3]:void 0})}this.drawMarkers(t,d,y,w,a,r,h),this.drawHover(t,d,y,_,o,r,i)}drawBands(t,o,r,i,a,l){if(a.length!==0){t.save(),t.font=S,t.textAlign="center";for(let s of a){let b=Math.max(0,Math.min(s.from,s.to)),u=Math.min(l-1,Math.max(s.from,s.to));if(u<b)continue;let m=Math.max(o.left,r(b)-i/2),f=Math.min(o.right,r(u)+i/2)-m;f<=0||(t.globalAlpha=s.alpha??.07,t.fillStyle=s.color,t.fillRect(m,o.top,f,o.height),t.globalAlpha=.85,t.fillRect(m,o.top-ce,f,We),s.label&&(t.globalAlpha=1,t.measureText(s.label).width+8<=f&&(t.textBaseline="bottom",t.fillText(s.label,m+f/2,o.top-ce-2))))}t.restore()}}drawAxisTitles(t,o,r,i){if(!r&&!i)return;let a=o.top+o.height/2;t.save(),t.font=S,t.fillStyle=M.text,t.textAlign="center",t.textBaseline="middle",r&&fe(t,r,Y/2,a),i&&fe(t,i,this.width-Y/2,a),t.restore()}drawMarkers(t,o,r,i,a,l,s){if(a.length===0)return;let b=16,u=i(A);t.save(),t.font=S;for(let m of a){if(m.index<0||m.index>=s)continue;let h=o.bottom;for(let g of l){let C=g.values[m.index];if(C==null||!Number.isFinite(C))continue;let d=i(Math.max(C,A));h=Math.min(h,u-(u-d)*this.reveal)}let f=Math.max(2,h-b-4),c=t.measureText(m.label).width+12,v=j(r(m.index),c,o.right);te(t,v,f,m.label,{background:m.background,text:m.color})}t.restore()}drawHover(t,o,r,i,a,l,s){if(i==null)return;let b=a[i];if(b==null)return;let u=r(i);Q(t,u,o.top,o.bottom),this.onHoverChange?.({index:i,category:b,x:u,bars:pe(l,i),lines:pe(s??[],i)})}};function fe(e,n,t,o){e.save(),e.translate(t,o),e.rotate(-Math.PI/2),e.fillText(n,0,0),e.restore()}function ke(e,n){let t=0;for(let o=0;o<n;o++){let r=e[o];r!=null&&Number.isFinite(r)&&r>t&&(t=r)}return t}function pe(e,n){let t=[];for(let o of e){let r=o.values[n];r!=null&&t.push({id:o.id,value:r,color:o.color})}return t}export{z as ComboChart};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { BarFillMode } from "../renderers/primitives/grouped-bars";
|
|
2
|
+
export type { BarFillMode };
|
|
3
|
+
export interface ComboBarSeries {
|
|
4
|
+
id: string;
|
|
5
|
+
color: string;
|
|
6
|
+
/** One entry per category, aligned to ComboChartData.categories. */
|
|
7
|
+
values: readonly (number | null)[];
|
|
8
|
+
/** Per-bar colour override. Missing entries fall back to `color`. */
|
|
9
|
+
colors?: readonly (string | null | undefined)[];
|
|
10
|
+
/** Per-category fill, aligned to `values`. Missing = "solid". */
|
|
11
|
+
fillModes?: readonly BarFillMode[];
|
|
12
|
+
}
|
|
13
|
+
export type ComboLineStyle = "solid" | "dashed";
|
|
14
|
+
export interface ComboLineOverlay {
|
|
15
|
+
id: string;
|
|
16
|
+
color: string;
|
|
17
|
+
values: readonly (number | null)[];
|
|
18
|
+
style?: ComboLineStyle;
|
|
19
|
+
/** "right" gives the line its own labelled axis. "auto" just stretches it to
|
|
20
|
+
* fill the pane, so you see its shape but read its value only on hover. */
|
|
21
|
+
axis: "right" | "auto";
|
|
22
|
+
/** How to write the right-axis numbers. Unused by "auto" lines. */
|
|
23
|
+
formatter?: (value: number) => string;
|
|
24
|
+
}
|
|
25
|
+
/** A small label above one category's bars. The caller writes the text; the
|
|
26
|
+
* chart only places it. */
|
|
27
|
+
export interface ComboMarker {
|
|
28
|
+
index: number;
|
|
29
|
+
label: string;
|
|
30
|
+
background: string;
|
|
31
|
+
color: string;
|
|
32
|
+
}
|
|
33
|
+
/** A tint across a run of categories, drawn behind everything, for marking a
|
|
34
|
+
* stretch as one period. `label` goes in the gutter above it when it fits. */
|
|
35
|
+
export interface ComboBand {
|
|
36
|
+
/** Inclusive category indices. */
|
|
37
|
+
from: number;
|
|
38
|
+
to: number;
|
|
39
|
+
color: string;
|
|
40
|
+
/** How strong the tint is. Kept faint by default so the bars stay readable. */
|
|
41
|
+
alpha?: number;
|
|
42
|
+
label?: string;
|
|
43
|
+
}
|
|
44
|
+
/** Extra bars beside the main ones, on their own scale and squashed into a
|
|
45
|
+
* strip at the bottom. For numbers far too small to show on the main axis;
|
|
46
|
+
* unlabelled, so their values only appear on hover. */
|
|
47
|
+
export interface ComboSubScaleBars {
|
|
48
|
+
id: string;
|
|
49
|
+
color: string;
|
|
50
|
+
values: readonly (number | null)[];
|
|
51
|
+
}
|
|
52
|
+
export interface ComboChartData {
|
|
53
|
+
categories: readonly string[];
|
|
54
|
+
bars: readonly ComboBarSeries[];
|
|
55
|
+
lineOverlays?: readonly ComboLineOverlay[];
|
|
56
|
+
markers?: readonly ComboMarker[];
|
|
57
|
+
bands?: readonly ComboBand[];
|
|
58
|
+
subBars?: ComboSubScaleBars;
|
|
59
|
+
leftFormatter?: (value: number) => string;
|
|
60
|
+
/** Axis names, drawn sideways next to the numbers. Omit for none. */
|
|
61
|
+
leftAxisLabel?: string;
|
|
62
|
+
rightAxisLabel?: string;
|
|
63
|
+
}
|
|
64
|
+
export interface ComboHoverPoint {
|
|
65
|
+
id: string;
|
|
66
|
+
value: number;
|
|
67
|
+
color: string;
|
|
68
|
+
}
|
|
69
|
+
export interface ComboHoverEvent {
|
|
70
|
+
index: number;
|
|
71
|
+
category: string;
|
|
72
|
+
x: number;
|
|
73
|
+
bars: readonly ComboHoverPoint[];
|
|
74
|
+
lines: readonly ComboHoverPoint[];
|
|
75
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export declare const CHART_CONFIG: {
|
|
2
|
+
readonly layout: {
|
|
3
|
+
readonly priceScaleWidth: 60;
|
|
4
|
+
readonly timeScaleHeight: 30;
|
|
5
|
+
};
|
|
6
|
+
readonly zoom: {
|
|
7
|
+
readonly default: 100;
|
|
8
|
+
readonly min: 10;
|
|
9
|
+
readonly max: 800;
|
|
10
|
+
readonly inFactor: 0.95;
|
|
11
|
+
readonly outFactor: 1.05;
|
|
12
|
+
};
|
|
13
|
+
readonly pan: {
|
|
14
|
+
/** How far the chart moves per pixel of scroll. Lower feels heavier. */
|
|
15
|
+
readonly wheelSensitivity: 1;
|
|
16
|
+
/** Some browsers measure a mouse wheel in lines, not pixels — this is
|
|
17
|
+
* how big we treat a line, so a wheel and a trackpad feel similar. */
|
|
18
|
+
readonly linesToPx: 16;
|
|
19
|
+
};
|
|
20
|
+
readonly viewport: {
|
|
21
|
+
/** Share of the view left empty to the right of the newest candle. */
|
|
22
|
+
readonly rightPaddingRatio: 0.2;
|
|
23
|
+
/** How far past the oldest candle you can scroll, as a share of the
|
|
24
|
+
* visible range — stops you panning into a blank screen. */
|
|
25
|
+
readonly maxLeftEmptyRatio: 0.8;
|
|
26
|
+
};
|
|
27
|
+
readonly infiniteScroll: {
|
|
28
|
+
/** Fetch older candles once this few remain to the left of the view.
|
|
29
|
+
* Raise it if the data source is slow. */
|
|
30
|
+
readonly triggerThreshold: 50;
|
|
31
|
+
};
|
|
32
|
+
readonly priceScale: {
|
|
33
|
+
/** Share of the visible price range added above and below, so candles
|
|
34
|
+
* never touch the top or bottom edge. */
|
|
35
|
+
readonly paddingRatio: 0.1;
|
|
36
|
+
};
|
|
37
|
+
/** Floors that keep the price pane usable no matter what panes are added.
|
|
38
|
+
* An indicator's own preferred height lives in its render spec, not here. */
|
|
39
|
+
readonly pane: {
|
|
40
|
+
/** Fallback height when a pane's spec doesn't state one. */
|
|
41
|
+
readonly defaultHeight: 120;
|
|
42
|
+
/** Hard floor for any sub-pane, whatever its spec says. */
|
|
43
|
+
readonly minHeight: 60;
|
|
44
|
+
/** The price pane never shrinks below this, however many panes exist. */
|
|
45
|
+
readonly minChartHeight: 120;
|
|
46
|
+
/** Px either side of a divider that still counts as grabbing it. */
|
|
47
|
+
readonly handleHitZone: 6;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
export declare const LAYOUT: {
|
|
51
|
+
readonly priceScaleWidth: 60;
|
|
52
|
+
readonly timeScaleHeight: 30;
|
|
53
|
+
};
|
|
54
|
+
export declare const ZOOM: {
|
|
55
|
+
readonly default: 100;
|
|
56
|
+
readonly min: 10;
|
|
57
|
+
readonly max: 800;
|
|
58
|
+
readonly inFactor: 0.95;
|
|
59
|
+
readonly outFactor: 1.05;
|
|
60
|
+
};
|
|
61
|
+
export declare const VIEWPORT: {
|
|
62
|
+
/** Share of the view left empty to the right of the newest candle. */
|
|
63
|
+
readonly rightPaddingRatio: 0.2;
|
|
64
|
+
/** How far past the oldest candle you can scroll, as a share of the
|
|
65
|
+
* visible range — stops you panning into a blank screen. */
|
|
66
|
+
readonly maxLeftEmptyRatio: 0.8;
|
|
67
|
+
};
|
|
68
|
+
export declare const PAN: {
|
|
69
|
+
/** How far the chart moves per pixel of scroll. Lower feels heavier. */
|
|
70
|
+
readonly wheelSensitivity: 1;
|
|
71
|
+
/** Some browsers measure a mouse wheel in lines, not pixels — this is
|
|
72
|
+
* how big we treat a line, so a wheel and a trackpad feel similar. */
|
|
73
|
+
readonly linesToPx: 16;
|
|
74
|
+
};
|
|
75
|
+
export declare const INFINITE_SCROLL: {
|
|
76
|
+
/** Fetch older candles once this few remain to the left of the view.
|
|
77
|
+
* Raise it if the data source is slow. */
|
|
78
|
+
readonly triggerThreshold: 50;
|
|
79
|
+
};
|
|
80
|
+
export declare const PRICE_SCALE: {
|
|
81
|
+
/** Share of the visible price range added above and below, so candles
|
|
82
|
+
* never touch the top or bottom edge. */
|
|
83
|
+
readonly paddingRatio: 0.1;
|
|
84
|
+
};
|
|
85
|
+
export declare const PANE: {
|
|
86
|
+
/** Fallback height when a pane's spec doesn't state one. */
|
|
87
|
+
readonly defaultHeight: 120;
|
|
88
|
+
/** Hard floor for any sub-pane, whatever its spec says. */
|
|
89
|
+
readonly minHeight: 60;
|
|
90
|
+
/** The price pane never shrinks below this, however many panes exist. */
|
|
91
|
+
readonly minChartHeight: 120;
|
|
92
|
+
/** Px either side of a divider that still counts as grabbing it. */
|
|
93
|
+
readonly handleHitZone: 6;
|
|
94
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var Z=Object.defineProperty;var j=(e,n,t)=>n in e?Z(e,n,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[n]=t;var f=(e,n,t)=>j(e,typeof n!="symbol"?n+"":n,t);var x={bullish:"#22c55e",bearish:"#ef4444",grid:"#313150",text:"#8b8b93",crosshair:"#a1a1aa",priceLine:"#3b82f6",pill:"#313150",pillText:"#ffffff",divider:"#505064",dividerActive:"#8c8cb9",background:"transparent"},Q=[["--chart-grid","grid"],["--chart-axis","text"],["--chart-crosshair","crosshair"],["--chart-1","priceLine"],["--chart-pill","pill"],["--chart-pill-foreground","pillText"],["--chart-divider","divider"],["--chart-divider-active","dividerActive"]];function w(){if(typeof document>"u")return;let e=getComputedStyle(document.documentElement);for(let[n,t]of Q){let r=e.getPropertyValue(n).trim();r&&(x[t]=r)}}function k(e,n,t){let r=e.getContext("2d");if(!r)throw new Error("Could not get 2D context");let o=new ResizeObserver(a=>{if(a.length===0)return;let{width:b,height:m}=a[0].contentRect;requestAnimationFrame(()=>t.onResize(Math.floor(b),Math.floor(m)))});o.observe(n),w();let i=new MutationObserver(()=>{w(),t.onThemeChange()});i.observe(document.documentElement,{attributes:!0,attributeFilter:["class"]});function s(a,b){let m=window.devicePixelRatio||1;r.setTransform(1,0,0,1,0,0),e.width=Math.floor(a*m),e.height=Math.floor(b*m),e.style.width=`${a}px`,e.style.height=`${b}px`,r.scale(m,m)}return{ctx:r,resizeBackingStore:s,destroy:()=>{o.disconnect(),i.disconnect()}}}var T=class{constructor(n,t){f(this,"canvas");f(this,"shell");f(this,"width",0);f(this,"height",0);f(this,"mouseX",null);f(this,"mouseY",null);f(this,"animationFrame",null);f(this,"onHoverChange");f(this,"handleMouseMove",n=>{let t=this.canvas.getBoundingClientRect();this.mouseX=n.clientX-t.left,this.mouseY=n.clientY-t.top,this.render()});f(this,"handleMouseLeave",()=>{this.mouseX=null,this.mouseY=null,this.onHoverChange?.(null),this.render()});this.canvas=n,this.shell=k(n,t,{onResize:(r,o)=>this.resize(r,o),onThemeChange:()=>this.render()}),n.addEventListener("mousemove",this.handleMouseMove),n.addEventListener("mouseleave",this.handleMouseLeave)}destroy(){this.cancelAnimation(),this.canvas.removeEventListener("mousemove",this.handleMouseMove),this.canvas.removeEventListener("mouseleave",this.handleMouseLeave),this.shell.destroy()}animate(n,t,r){this.cancelAnimation();let o=performance.now(),i=s=>{let a=Math.min(1,(s-o)/n);r(1-(1-a)**t),this.render(),this.animationFrame=a<1?requestAnimationFrame(i):null};this.animationFrame=requestAnimationFrame(i)}cancelAnimation(){this.animationFrame!=null&&(cancelAnimationFrame(this.animationFrame),this.animationFrame=null)}resize(n,t){n===0||t===0||(this.width=n,this.height=t,this.shell.resizeBackingStore(n,t),this.render())}};var S="11px Inter, sans-serif";function I(e,n,t){e.save(),e.font=S;let r=24;for(let i of n)r=Math.max(r,e.measureText(i).width);e.restore();let o=n.length>=2?t(1)-t(0):r+12;return Math.max(1,Math.ceil((r+16)/Math.max(1,o)))}function O(e,n,t,r,o){e.save(),e.font=S,e.textBaseline="top",e.textAlign="center",e.fillStyle=x.text;for(let i=0;i<n.length;i+=o)e.fillText(n[i],t(i),r+8);e.restore()}function _(e,n){let t=e>0?n/e:n;return{slotWidth:t,toX:r=>r*t+t/2}}function H(e,n,t){if(n===0)return null;let r=0,o=1/0;for(let i=0;i<n;i++){let s=Math.abs(t(i)-e);s<o&&(o=s,r=i)}return r}function L(e,n){let{ctx:t}=e;t.save(),t.beginPath(),t.rect(0,e.top,e.chartWidth,e.height),t.clip(),t.beginPath(),n(t),t.restore()}function E(e,n){e.strokeStyle=n.color,e.fillStyle=n.color,e.lineWidth=n.lineWidth??1.5,e.globalAlpha=n.opacity??1,e.setLineDash(n.dash??[])}function Y(e,n,t,r){e.save(),e.strokeStyle=x.crosshair,e.lineWidth=1,e.setLineDash([4,4]),e.beginPath(),e.moveTo(n,t),e.lineTo(n,r),e.stroke(),e.restore()}function N(e,n){let t=e/Math.max(1,n),r=Math.pow(10,Math.floor(Math.log10(Math.max(t,1e-10)))),o=t/r,i;return o<1.5?i=1:o<3.5?i=2:o<7.5?i=5:i=10,i*r}function X(e,n,t,r){if(e.mode==="price")return null;let o=1/0,i=-1/0;for(let l of n){let d=Math.min(r,l.length);for(let h=Math.max(0,t);h<d;h++){let u=l[h];u==null||!Number.isFinite(u)||(u<o&&(o=u),u>i&&(i=u))}}let s=Number.isFinite(o)&&Number.isFinite(i);if(e.mode==="auto"){if(!s)return null;let l=Math.max(i-o,1e-10),d=Math.max(l*e.cushionRatio,e.minCushion??0);return{min:o-d,max:i+d}}if(!s){let[l,d]=e.alwaysInclude??[e.min,e.max];return{min:l,max:d}}let a=i-o,b=Math.max(a*e.cushionRatio,e.minCushion??0),m=o-b,p=i+b;return e.alwaysInclude&&(m=Math.min(m,e.alwaysInclude[0]),p=Math.max(p,e.alwaysInclude[1])),{min:Math.max(m,e.min),max:Math.min(p,e.max)}}function D(e,n,t,r=8,o=8){let i=Math.max(1,t-r-o),s=Math.max(e.max-e.min,1e-10);return a=>n+r+(e.max-a)/s*i}function B(e,n,t,r){let o=Math.max(1,r);return{left:e,right:n,top:t,width:Math.max(1,n-e),height:o,bottom:t+o}}function M(e){return Math.round(e)+.5}function A(e,n,t){return X({mode:"auto",cushionRatio:t},e,0,n)??{min:0,max:1}}function V(e,n){let t=N(e.max-e.min,Math.max(2,Math.round(n/45))),r=[];for(let o=Math.ceil(e.min/t)*t;o<=e.max;o+=t)r.push(o);return r}function $(e,n,t,r={}){let{colorMode:o="fixed",momentumColors:i,colorValues:s}=r;L(e,a=>{E(a,t),a.lineJoin="round",a.lineCap="round";let b=o==="directional"&&!!i,m=Math.min(e.endIndex,n.length),p=null,l=null,d=null;for(let h=Math.max(0,e.startIndex);h<m;h++){let u=n[h];if(u==null||!Number.isFinite(u)){p=null,l=null,d=null;continue}let g=e.toX(h),v=e.toY(u),C=s?.[h],y=C!=null&&C<0,P=b&&d!==null&&y!==d;if(p==null||l==null||P){b||a.moveTo(g,v),p=g,l=v,d=y;continue}b?(a.strokeStyle=y?i.bearStrong:i.bullStrong,a.beginPath(),a.moveTo(p,l),a.lineTo(g,v),a.stroke()):a.lineTo(g,v),p=g,l=v,d=y}b||a.stroke()})}function R(e,n,t){let r=t.side==="left";e.save(),e.font=S,e.lineWidth=1,e.textBaseline="middle",e.textAlign=r?"right":"left";let o=r?n.left-8:n.right+8;for(let i of V(t.extent,n.height)){let s=t.toY(i);s<n.top||s>n.bottom||(t.gridlines&&(e.strokeStyle=x.grid,e.beginPath(),e.moveTo(n.left,M(s)),e.lineTo(n.right,M(s)),e.stroke()),e.fillStyle=x.text,e.fillText(t.format(i),o,s))}e.restore()}function K(e,n){e.save(),e.strokeStyle=x.grid,e.lineWidth=1,e.beginPath(),e.moveTo(n.left,M(n.bottom)),e.lineTo(n.right,M(n.bottom)),e.stroke(),e.restore()}function z(e,n,t,r,o){e.save(),e.strokeStyle=x.grid,e.lineWidth=1;for(let i=0;i<t;i+=o){let s=M(r(i));e.beginPath(),e.moveTo(s,n.top),e.lineTo(s,n.bottom),e.stroke()}e.restore()}var ee=56,G=60,te=30,J=8,ne=650,F=class e extends T{constructor(){super(...arguments);f(this,"data",{categories:[],series:[]});f(this,"revealProgress",1);f(this,"shapeKey",null)}setData(t){let r=e.computeShapeKey(t),o=r===this.shapeKey;if(this.data=t,this.shapeKey=r,o||t.categories.length===0){this.revealProgress=1,this.render();return}this.revealProgress=0,this.animate(ne,2,i=>{this.revealProgress=i})}static computeShapeKey(t){return`${t.categories.length}|${t.series.map(r=>`${r.id}:${r.axis}`).join(",")}`}render(){let{ctx:t}=this.shell;if(this.width===0||this.height===0)return;t.clearRect(0,0,this.width,this.height);let{categories:r,series:o,leftFormatter:i=c=>c.toFixed(2),rightFormatter:s=c=>c.toLocaleString()}=this.data;if(r.length===0)return;let a=r.length,b=o.filter(c=>c.axis==="left"),m=o.filter(c=>c.axis==="right"),p=m.length>0,l=B(ee,this.width-(p?G:8),J,this.height-J-te),d=_(a,l.width),h=c=>l.left+d.toX(c),u=A(b.map(c=>c.values),a,.08),g=A(m.map(c=>c.values),a,.08),v={min:Math.min(g.min,0),max:g.max},C=D(u,l.top,l.height,0,0),y=D(v,l.top,l.height,0,0);p&&R(t,l,{extent:v,toY:y,format:s,side:"right",gridlines:!0}),R(t,l,{extent:u,toY:C,format:i,side:"left",gridlines:!p});let P=I(t,r,h);z(t,l,a,h,P),K(t,l),O(t,r,h,l.bottom,P);let U=Math.max(1,Math.ceil(a*this.revealProgress)),q={ctx:t,chartWidth:l.right,priceScaleWidth:G,top:l.top,height:l.height,extent:u,candleWidth:d.slotWidth,startIndex:0,endIndex:U,toX:h,toY:C};for(let c of o){let W=c.axis==="left";$({...q,extent:W?u:v,toY:W?C:y},c.values,{color:c.color,lineWidth:c.emphasis?1.75:1.5})}this.drawHover(t,l,h,C,y,r,o)}drawHover(t,r,o,i,s,a,b){if(this.mouseX==null)return;let m=H(this.mouseX,a.length,o);if(m==null)return;let p=a[m];if(p==null)return;let l=o(m);Y(t,l,r.top,r.bottom);let d=[];for(let h of b){let u=h.values[m];if(u==null)continue;let g=h.axis==="left"?i(u):s(u);d.push({id:h.id,value:u,x:l,y:g})}this.onHoverChange?.({index:m,category:p,x:l,points:d})}};export{F as LineChart};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CanvasChartEngine } from "../canvas-chart-base";
|
|
2
|
+
import type { LineChartData, LineHoverEvent } from "./types";
|
|
3
|
+
/** A few line series over a fixed list of labels, with an independent scale on
|
|
4
|
+
* each side. Draws its own two-sided axes; the rest of the package has one. */
|
|
5
|
+
export declare class LineChart extends CanvasChartEngine<LineHoverEvent> {
|
|
6
|
+
private data;
|
|
7
|
+
/** 0-1, how much of the X range the lines are drawn out to — animated from
|
|
8
|
+
* 0 on a fresh draw so it reads as data arriving, not a jump cut. */
|
|
9
|
+
private revealProgress;
|
|
10
|
+
/** The previous data's shape (category count + series ids/axes): a same-shape
|
|
11
|
+
* update is just new values for the same lines, so it updates in place
|
|
12
|
+
* instead of replaying the reveal. */
|
|
13
|
+
private shapeKey;
|
|
14
|
+
setData(data: LineChartData): void;
|
|
15
|
+
private static computeShapeKey;
|
|
16
|
+
protected render(): void;
|
|
17
|
+
/** Crosshair plus one screen-space point per series at the hovered index —
|
|
18
|
+
* the tooltip itself is a DOM overlay owned by whoever consumes
|
|
19
|
+
* onHoverChange, same split as the categorical bar chart's hover. */
|
|
20
|
+
private drawHover;
|
|
21
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface LineSeriesSpec {
|
|
2
|
+
id: string;
|
|
3
|
+
color: string;
|
|
4
|
+
/** One entry per category, aligned to LineChartData.categories. */
|
|
5
|
+
values: readonly (number | null)[];
|
|
6
|
+
axis: "left" | "right";
|
|
7
|
+
/** Drawn a touch thicker, to make one line stand out from the rest. */
|
|
8
|
+
emphasis?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface LineChartData {
|
|
11
|
+
/** X-axis labels, in display order — usually formatted time buckets. */
|
|
12
|
+
categories: readonly string[];
|
|
13
|
+
series: readonly LineSeriesSpec[];
|
|
14
|
+
leftFormatter?: (value: number) => string;
|
|
15
|
+
rightFormatter?: (value: number) => string;
|
|
16
|
+
}
|
|
17
|
+
/** One series' value at the hovered spot, plus where it sits on screen — so
|
|
18
|
+
* each line can have its own little tooltip. */
|
|
19
|
+
export interface LineHoverPoint {
|
|
20
|
+
id: string;
|
|
21
|
+
value: number;
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
}
|
|
25
|
+
export interface LineHoverEvent {
|
|
26
|
+
index: number;
|
|
27
|
+
category: string;
|
|
28
|
+
x: number;
|
|
29
|
+
points: readonly LineHoverPoint[];
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var E=Object.defineProperty;var T=(s,n,e)=>n in s?E(s,n,{enumerable:!0,configurable:!0,writable:!0,value:e}):s[n]=e;var u=(s,n,e)=>T(s,typeof n!="symbol"?n+"":n,e);var b={bullish:"#22c55e",bearish:"#ef4444",grid:"#313150",text:"#8b8b93",crosshair:"#a1a1aa",priceLine:"#3b82f6",pill:"#313150",pillText:"#ffffff",divider:"#505064",dividerActive:"#8c8cb9",background:"transparent"},L=[["--chart-grid","grid"],["--chart-axis","text"],["--chart-crosshair","crosshair"],["--chart-1","priceLine"],["--chart-pill","pill"],["--chart-pill-foreground","pillText"],["--chart-divider","divider"],["--chart-divider-active","dividerActive"]];function f(){if(typeof document>"u")return;let s=getComputedStyle(document.documentElement);for(let[n,e]of L){let t=s.getPropertyValue(n).trim();t&&(b[e]=t)}}function g(s,n,e){let t=s.getContext("2d");if(!t)throw new Error("Could not get 2D context");let r=new ResizeObserver(i=>{if(i.length===0)return;let{width:a,height:l}=i[0].contentRect;requestAnimationFrame(()=>e.onResize(Math.floor(a),Math.floor(l)))});r.observe(n),f();let o=new MutationObserver(()=>{f(),e.onThemeChange()});o.observe(document.documentElement,{attributes:!0,attributeFilter:["class"]});function h(i,a){let l=window.devicePixelRatio||1;t.setTransform(1,0,0,1,0,0),s.width=Math.floor(i*l),s.height=Math.floor(a*l),s.style.width=`${i}px`,s.style.height=`${a}px`,t.scale(l,l)}return{ctx:t,resizeBackingStore:h,destroy:()=>{r.disconnect(),o.disconnect()}}}var p=class{constructor(n,e){u(this,"canvas");u(this,"shell");u(this,"width",0);u(this,"height",0);u(this,"mouseX",null);u(this,"mouseY",null);u(this,"animationFrame",null);u(this,"onHoverChange");u(this,"handleMouseMove",n=>{let e=this.canvas.getBoundingClientRect();this.mouseX=n.clientX-e.left,this.mouseY=n.clientY-e.top,this.render()});u(this,"handleMouseLeave",()=>{this.mouseX=null,this.mouseY=null,this.onHoverChange?.(null),this.render()});this.canvas=n,this.shell=g(n,e,{onResize:(t,r)=>this.resize(t,r),onThemeChange:()=>this.render()}),n.addEventListener("mousemove",this.handleMouseMove),n.addEventListener("mouseleave",this.handleMouseLeave)}destroy(){this.cancelAnimation(),this.canvas.removeEventListener("mousemove",this.handleMouseMove),this.canvas.removeEventListener("mouseleave",this.handleMouseLeave),this.shell.destroy()}animate(n,e,t){this.cancelAnimation();let r=performance.now(),o=h=>{let i=Math.min(1,(h-r)/n);t(1-(1-i)**e),this.render(),this.animationFrame=i<1?requestAnimationFrame(o):null};this.animationFrame=requestAnimationFrame(o)}cancelAnimation(){this.animationFrame!=null&&(cancelAnimationFrame(this.animationFrame),this.animationFrame=null)}resize(n,e){n===0||e===0||(this.width=n,this.height=e,this.shell.resizeBackingStore(n,e),this.render())}};var y="bold 11px Inter, sans-serif";var v=Math.PI*2,S=-Math.PI/2,M=6,x=8+M,w=600;function D(s,n){return s.slices.length===n.slices.length&&s.slices.every((e,t)=>e.id===n.slices[t].id)}function H(s,n,e){return{...n,slices:n.slices.map((t,r)=>{let o=s.slices[r]?.value,h=o!=null&&Number.isFinite(o)?o+(t.value-o)*e:t.value;return{...t,value:h}})}}function A(s,n){let e=s;for(;e<n;)e+=v;for(;e>=n+v;)e-=v;return e}var C=class extends p{constructor(){super(...arguments);u(this,"data",{slices:[]})}setData(e){let t=this.data;if(t.slices.length===0||!D(t,e)){this.data=e,this.render();return}this.animate(w,1.5,r=>{this.data=H(t,e,r)})}render(){let{ctx:e}=this.shell;if(this.width===0||this.height===0)return;e.clearRect(0,0,this.width,this.height);let{slices:t}=this.data,r=t.reduce((m,c)=>m+Math.max(0,c.value),0);if(t.length===0||r<=0)return;let o=this.width/2,h=this.height/2,i=Math.min(this.width,this.height)/2-x,a=i*Math.max(0,Math.min(1,this.data.innerRadiusRatio??0));if(i<=0)return;let l=this.computeGeometry(t,r),d=this.findHoverIndex(o,h,a,i,l);l.forEach((m,c)=>{if(!m)return;let P=i+(c===d?M:0);this.drawSlice(e,o,h,a,P,m.start,m.end,t[c].color)}),a>0&&this.data.centerLabel&&this.drawCenterLabel(e,o,h,this.data.centerLabel),this.emitHover(d,l,t,o,h,i)}computeGeometry(e,t){let r=S;return e.map(o=>{let h=Math.max(0,o.value);if(h===0)return null;let i=h/t,a=r,l=a+i*v;return r=l,{start:a,end:l,fraction:i}})}drawSlice(e,t,r,o,h,i,a,l){e.beginPath(),o>0?(e.arc(t,r,h,i,a,!1),e.arc(t,r,o,a,i,!0)):(e.moveTo(t,r),e.arc(t,r,h,i,a,!1)),e.closePath(),e.fillStyle=l,e.fill()}drawCenterLabel(e,t,r,o){e.save(),e.font=y,e.fillStyle=b.text,e.textAlign="center",e.textBaseline="middle",e.fillText(o,t,r),e.restore()}findHoverIndex(e,t,r,o,h){if(this.mouseX==null||this.mouseY==null)return null;let i=this.mouseX-e,a=this.mouseY-t,l=Math.hypot(i,a);if(l<r||l>o)return null;let d=A(Math.atan2(a,i),S),m=h.findIndex(c=>c!=null&&d>=c.start&&d<c.end);return m===-1?null:m}emitHover(e,t,r,o,h,i){if(e==null){this.onHoverChange?.(null);return}let a=t[e],l=r[e];if(!a||!l){this.onHoverChange?.(null);return}let d=(a.start+a.end)/2,m=o+Math.cos(d)*i,c=h+Math.sin(d)*i;this.onHoverChange?.({index:e,slice:l,fraction:a.fraction,x:m,y:c})}};export{C as PieChart};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CanvasChartEngine } from "../canvas-chart-base";
|
|
2
|
+
import type { PieChartData, PieHoverEvent } from "./types";
|
|
3
|
+
/** A single ring of slices sized by value, no axes — there's nothing to
|
|
4
|
+
* scale against but the whole. `innerRadiusRatio` turns it into a donut. */
|
|
5
|
+
export declare class PieChart extends CanvasChartEngine<PieHoverEvent> {
|
|
6
|
+
private data;
|
|
7
|
+
setData(data: PieChartData): void;
|
|
8
|
+
protected render(): void;
|
|
9
|
+
/** One entry per slice, aligned by index — null for a zero/negative value,
|
|
10
|
+
* which draws nothing and can never be hovered. */
|
|
11
|
+
private computeGeometry;
|
|
12
|
+
private drawSlice;
|
|
13
|
+
private drawCenterLabel;
|
|
14
|
+
/** Which slice (if any) the cursor sits over — outside the ring, or inside
|
|
15
|
+
* a donut's hole, is nobody's. */
|
|
16
|
+
private findHoverIndex;
|
|
17
|
+
/** The value breakdown is a DOM tooltip owned by whoever consumes
|
|
18
|
+
* onHoverChange, same split as every other chart in this package —
|
|
19
|
+
* x/y here just tell it where to sit. */
|
|
20
|
+
private emitHover;
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface PieSlice {
|
|
2
|
+
id: string;
|
|
3
|
+
label: string;
|
|
4
|
+
value: number;
|
|
5
|
+
color: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PieChartData {
|
|
8
|
+
slices: readonly PieSlice[];
|
|
9
|
+
/** How to write each slice's value in the hover event. Defaults to a plain
|
|
10
|
+
* formatted number. */
|
|
11
|
+
valueFormatter?: (value: number) => string;
|
|
12
|
+
/** 0 (the default) draws a full pie. A donut with a hole this fraction of
|
|
13
|
+
* the outer radius otherwise — e.g. 0.6. */
|
|
14
|
+
innerRadiusRatio?: number;
|
|
15
|
+
/** Short text drawn in the donut's hole (e.g. a total). Ignored when
|
|
16
|
+
* innerRadiusRatio is 0 — a full pie has no hole to put it in. */
|
|
17
|
+
centerLabel?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface PieHoverEvent {
|
|
20
|
+
index: number;
|
|
21
|
+
slice: PieSlice;
|
|
22
|
+
/** This slice's share of the total of all positive values, 0-1. */
|
|
23
|
+
fraction: number;
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PriceExtent } from "./types";
|
|
2
|
+
export interface PlotFrame {
|
|
3
|
+
left: number;
|
|
4
|
+
right: number;
|
|
5
|
+
top: number;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
/** y of the category axis — where bars and lines bottom out. */
|
|
9
|
+
bottom: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function createPlotFrame(left: number, right: number, top: number, height: number): PlotFrame;
|
|
12
|
+
/** Nudges a coordinate half a pixel. A 1px line drawn on a whole pixel gets
|
|
13
|
+
* smeared across two rows and looks washed out. */
|
|
14
|
+
export declare function snapPixel(value: number): number;
|
|
15
|
+
/** Value range fitting every series, with room to spare. Falls back to 0-1
|
|
16
|
+
* when there's nothing to fit. */
|
|
17
|
+
export declare function autoExtent(valueSets: readonly (readonly (number | null)[])[], categoryCount: number, cushionRatio: number): PriceExtent;
|
|
18
|
+
/** Tick values spanning `extent` at round steps, aiming for one per ~45px. */
|
|
19
|
+
export declare function axisTicks(extent: PriceExtent, plotHeight: number): number[];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
declare const MONTH_NAMES: readonly ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
2
|
+
declare const DASH_PATTERN: readonly [4, 4];
|
|
3
|
+
declare const SOLID_PATTERN: number[];
|
|
4
|
+
declare const AXIS_FONT = "11px Inter, sans-serif";
|
|
5
|
+
declare const AXIS_FONT_BOLD = "bold 11px Inter, sans-serif";
|
|
6
|
+
declare const LEGEND_FONT = "15px 'Inter', sans-serif";
|
|
7
|
+
/** Price -> Y pixel. Build it once a frame and pass it around, so every draw
|
|
8
|
+
* lands on exactly the same pixels. */
|
|
9
|
+
declare function makeMapY(minPrice: number, maxPrice: number, chartHeight: number): (price: number) => number;
|
|
10
|
+
declare function formatDateTime(timestamp: number): string;
|
|
11
|
+
declare function formatVolume(v: number): string;
|
|
12
|
+
/** Timestamp for a candle index. Indexes past either end of the data are
|
|
13
|
+
* guessed from the spacing, so labels keep going into the empty space. */
|
|
14
|
+
declare function timeAtIndex(data: {
|
|
15
|
+
time: number;
|
|
16
|
+
}[], index: number): number | null;
|
|
17
|
+
export { makeMapY, formatDateTime, formatVolume, timeAtIndex, MONTH_NAMES, DASH_PATTERN, SOLID_PATTERN, AXIS_FONT, AXIS_FONT_BOLD, LEGEND_FONT, };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DrawContext } from "./context";
|
|
2
|
+
export interface AxisPill {
|
|
3
|
+
value: number;
|
|
4
|
+
/** Background colour — the text is always white. */
|
|
5
|
+
color: string;
|
|
6
|
+
text: string;
|
|
7
|
+
/** "full" fills the whole gutter width; "snug" wraps the text. */
|
|
8
|
+
variant?: "snug" | "full";
|
|
9
|
+
}
|
|
10
|
+
/** Draws a pane's labels together, so they can be spaced out as a group. */
|
|
11
|
+
export declare function drawAxisPills(c: DrawContext, pills: readonly AxisPill[]): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PriceExtent } from "../../types";
|
|
2
|
+
export interface DrawContext {
|
|
3
|
+
ctx: CanvasRenderingContext2D;
|
|
4
|
+
/** Where the plot area ends and the value gutter begins. */
|
|
5
|
+
chartWidth: number;
|
|
6
|
+
priceScaleWidth: number;
|
|
7
|
+
/** The pane being drawn into. */
|
|
8
|
+
top: number;
|
|
9
|
+
height: number;
|
|
10
|
+
/** The value range spread across that pane's height. */
|
|
11
|
+
extent: PriceExtent;
|
|
12
|
+
/** Candle index -> canvas X, at the middle of the candle. */
|
|
13
|
+
toX: (absIndex: number) => number;
|
|
14
|
+
/** Value -> canvas Y inside this pane. */
|
|
15
|
+
toY: (value: number) => number;
|
|
16
|
+
/** Width of one candle's slot, in px. */
|
|
17
|
+
candleWidth: number;
|
|
18
|
+
/** The visible slice, as indexes into the full data array. */
|
|
19
|
+
startIndex: number;
|
|
20
|
+
endIndex: number;
|
|
21
|
+
}
|
|
22
|
+
export interface PlotStyle {
|
|
23
|
+
color: string;
|
|
24
|
+
lineWidth?: number;
|
|
25
|
+
/** 0-1, applied separately so it works with any colour format. */
|
|
26
|
+
opacity?: number;
|
|
27
|
+
dash?: number[];
|
|
28
|
+
}
|
|
29
|
+
/** Runs `draw` clipped to the pane, and tidies the canvas up afterwards. */
|
|
30
|
+
export declare function withPaneClip(c: DrawContext, draw: (ctx: CanvasRenderingContext2D) => void): void;
|
|
31
|
+
export declare function applyStyle(ctx: CanvasRenderingContext2D, style: PlotStyle): void;
|
|
32
|
+
/** The most recent real value up to `endIndex`, and which bar it was on. */
|
|
33
|
+
export declare function lastValueBefore(values: readonly (number | null)[], endIndex: number): {
|
|
34
|
+
value: number;
|
|
35
|
+
index: number;
|
|
36
|
+
} | null;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type DrawContext } from "./context";
|
|
2
|
+
/** A horizontal line at a fixed value, optionally named in the side gutter. */
|
|
3
|
+
export interface ReferenceLine {
|
|
4
|
+
value: number;
|
|
5
|
+
color: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
/** Label colour; falls back to the line's own. */
|
|
8
|
+
labelColor?: string;
|
|
9
|
+
dash?: number[];
|
|
10
|
+
lineWidth?: number;
|
|
11
|
+
}
|
|
12
|
+
/** A filled horizontal band between two values. */
|
|
13
|
+
export interface Zone {
|
|
14
|
+
from: number;
|
|
15
|
+
to: number;
|
|
16
|
+
color: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function drawZones(c: DrawContext, zones: readonly Zone[]): void;
|
|
19
|
+
export declare function drawReferenceLines(c: DrawContext, lines: readonly ReferenceLine[]): void;
|
|
20
|
+
/** Centres a label on `x`, nudged inward if it would hang off either edge. */
|
|
21
|
+
export declare function clampPillCenter(x: number, pillWidth: number, chartWidth: number): number;
|
|
22
|
+
/** Dashed vertical line at the hovered x, spanning one pane. */
|
|
23
|
+
export declare function drawCrosshairLine(ctx: CanvasRenderingContext2D, x: number, top: number, bottom: number): void;
|
|
24
|
+
/** Rounds a gap between gridlines to something a person would pick — 1, 2, 5
|
|
25
|
+
* or 10, times a power of ten. */
|
|
26
|
+
export declare function niceStep(range: number, targetLines: number): number;
|
|
27
|
+
/** Horizontal grid lines with their values, roughly one every 40px. */
|
|
28
|
+
export declare function drawPaneGrid(c: DrawContext, format?: (value: number) => string): void;
|
|
29
|
+
/** Line along a pane's top edge, with a little grip showing it can be dragged. */
|
|
30
|
+
export declare function drawPaneDivider(c: DrawContext, fullWidth: number, highlighted: boolean): void;
|
|
31
|
+
export interface PillStyle {
|
|
32
|
+
background: string;
|
|
33
|
+
text: string;
|
|
34
|
+
font?: string;
|
|
35
|
+
}
|
|
36
|
+
/** A small rounded label hung from (centerX, topY). Returns its width, so a
|
|
37
|
+
* caller placing several in a row knows where the next one starts. */
|
|
38
|
+
export declare function drawLabelPill(ctx: CanvasRenderingContext2D, centerX: number, topY: number, text: string, style: PillStyle, options?: {
|
|
39
|
+
paddingX?: number;
|
|
40
|
+
height?: number;
|
|
41
|
+
radius?: number;
|
|
42
|
+
}): number;
|
|
43
|
+
/** Traces a rounded rectangle. Caller fills or strokes it. */
|
|
44
|
+
export declare function roundRect(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number): void;
|