diff --git a/README.md b/README.md index 956f9ac..7d5c225 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,39 @@ Lightweight, canvas‑based charting library written in TypeScript, designed for ## Features -- 💡 **Zero external rendering deps** – plain `` -- ⚡ **Real‑time friendly** – redraws only what’s needed -- 📦 **ESM ready** – authored as native ES modules -- 📝 **TypeScript types** included (`*.d.ts`) -- 🎛️ Configurable axes, ticks, colors & cursor read‑outs +- 💡 **Zero external deps** – plain `` +- ⚡ **Real‑time friendly** – redraws only what’s needed +- 📦 **ESM ready** – authored as native ES modules +- 📝 **TypeScript types** included (`*.d.ts`) +- 🎛️ Configurable axes, ticks, colors, date formatting & cursor read‑outs + +## Options + +The `createChartElement(seriesList, options)` function supports the following options: + +| Option | Type | Description | +|------------------|-------------------------------------|-----------------------------------------------------------------------------| +| `width` | `number` | Fixed chart width in pixels (optional, defaults to container width) | +| `height` | `number` | Fixed chart height in pixels (optional, defaults to container height) | +| `min` | `number` | Minimum Y-axis value (optional, auto-computed if not set) | +| `max` | `number` | Maximum Y-axis value (optional, auto-computed if not set) | +| `showYAxis` | `boolean` | Whether to render Y-axis ticks and labels | +| `yTicks` | `number` | Number of Y-axis ticks (default: `5`) | +| `backgroundColor`| `string` | Optional background fill color | +| `maxTimeDelta` | `number \| null` | Gap in ms that breaks line continuity (default: `600`) | +| `dateFormat` | `(ts: number) => string` | Custom formatter for the cursor timestamp label | + +```ts +// Example: format timestamp to full Czech date+time +dateFormat: (ts) => new Date(ts).toLocaleString('cs-CZ', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + day: '2-digit', + month: '2-digit', + year: 'numeric' +}) +``` ## Installation @@ -25,47 +53,61 @@ npm install @meteolab/uchart - - uChart Interactive Test - + + uChart Interactive Test +
+ ``` ## Test diff --git a/src/index.d.ts b/dist/index.d.ts similarity index 83% rename from src/index.d.ts rename to dist/index.d.ts index 60bb2a8..1041560 100644 --- a/src/index.d.ts +++ b/dist/index.d.ts @@ -11,6 +11,8 @@ export declare function drawChart(ctx: CanvasRenderingContext2D, seriesList: Cha yTicks?: number; backgroundColor?: string; cursorX?: number | null; + maxTimeDelta?: number | null; + dateFormat?: (ts: number) => string; }): void; export declare function createChartElement(seriesList: ChartSeries[], opts: { width?: number; @@ -20,6 +22,8 @@ export declare function createChartElement(seriesList: ChartSeries[], opts: { showYAxis?: boolean; yTicks?: number; backgroundColor?: string; + maxTimeDelta?: number | null; + dateFormat?: (ts: number) => string; }): { element: HTMLDivElement; setSeries(series: ChartSeries[]): void; diff --git a/dist/uchart.js b/dist/uchart.js index 29de04a..efb20e7 100644 --- a/dist/uchart.js +++ b/dist/uchart.js @@ -1 +1 @@ -function W(t,m,a){let{width:o,height:d,min:F,max:n,showYAxis:T=!1,yTicks:w=5,cursorX:l=null}=a,k=10,E=d-k-10,x=m.flatMap(s=>s.data.map(i=>i[1])),y=m.flatMap(s=>s.data.map(i=>i[0])),p=F!==void 0?F:Math.min(...x),P=n!==void 0?n:Math.max(...x),e=P-p||1,r=Math.min(...y),A=Math.max(...y)-r||1;if(t.clearRect(0,0,o,d),T){t.beginPath(),t.strokeStyle="#aaa",t.lineWidth=1,t.font="15px sans-serif",t.fillStyle=m[0]?.strokeColor||"#000";for(let s=0;s<=w;s++){let i=p+e*s/w,c=k+(1-(i-p)/e)*E;t.moveTo(0,c),t.lineTo(5,c),t.fillText(i.toFixed(2),8,c+5)}t.stroke()}for(let s of m){let i=s.data,c=s.strokeColor||"#000";t.beginPath(),t.strokeStyle=c,t.lineWidth=1;let g=!1,C=null,b=600;for(let v=0;vP){g=!1,C=null;continue}C!=null&&h-C>b&&(g=!1);let u=(h-r)/A*o,M=k+(1-(f-p)/e)*E;g?t.lineTo(u,M):(t.moveTo(u,M),g=!0),C=h}t.stroke()}if(l!==null&&l>=0&&l<=o){let s=r+l/o*A;t.beginPath(),t.strokeStyle="#888",t.lineWidth=1,t.moveTo(l,0),t.lineTo(l,d),t.stroke(),t.font="12px sans-serif";let i=15;t.fillStyle="#fff",t.fillText(new Date(s).toLocaleTimeString(),l+6,i),i+=15;for(let c of m){let{data:g,strokeColor:C="#fff"}=c,b=g[0],v=Math.abs(b[0]-s);for(let h=1;h{e.style.position="absolute",e.style.top=e.style.left="0",e.style.width=e.style.height="100%",a.appendChild(e)}),d.style.pointerEvents="none";let F=o.getContext("2d"),n=d.getContext("2d"),T=t,w={...m},l=null,k=!1,S=()=>{let e=a.clientWidth,r=a.clientHeight;!e||!r||o.width===e&&o.height===r||(o.width=e,o.height=r,d.width=e,d.height=r,x(),y(l))},E=typeof ResizeObserver<"u"?new ResizeObserver(S):null;E?.observe(a),window.addEventListener("resize",S);let x=()=>{W(F,T,{...w,width:o.width,height:o.height})},y=e=>{if(n.clearRect(0,0,d.width,d.height),e==null)return;let r=T.flatMap(u=>u.data.map(M=>M[1])),Y=T.flatMap(u=>u.data.map(M=>M[0])),A=w.min??Math.min(...r),i=(w.max??Math.max(...r))-A||1,c=Math.min(...Y),C=Math.max(...Y)-c||1,b=c+e/o.width*C;n.beginPath(),n.strokeStyle="#888",n.lineWidth=1,n.moveTo(e,0),n.lineTo(e,d.height),n.stroke(),n.font="12px sans-serif",n.fillStyle="#fff",n.fillText(new Date(b).toLocaleTimeString(),e>o.width-100?e-100:e+6,15);let v=10,f=d.height-v-10,z=30;for(let{data:u,strokeColor:M="#fff"}of T){let L=u[0],D=Math.abs(L[0]-b);for(let R=1;Ro.width-100?e-100:e+6,z),z+=15;let V=v+(1-(L[1]-A)/i)*f;n.beginPath(),n.arc(e,V,3,0,Math.PI*2),n.fill()}},p=e=>{let r=a.getBoundingClientRect(),Y=o.width/r.width;l=(e.clientX-r.left)*Y,k||(k=!0,requestAnimationFrame(()=>{y(l),k=!1}))},P=()=>{l=null,y(null)};return a.addEventListener("mousemove",p),a.addEventListener("mouseleave",P),setTimeout(S,0),{element:a,setSeries(e){T=e,S(),x(),y(l)},setOptions(e){w={...w,...e},S(),x(),y(l)},destroy(){E?.disconnect(),window.removeEventListener("resize",S),a.removeEventListener("mousemove",p),a.removeEventListener("mouseleave",P)}}}export{X as createChartElement,W as drawChart}; +function N(t,u,o){let{width:a,height:m,min:Y,max:n,showYAxis:x=!1,yTicks:w=5,cursorX:s=null,maxTimeDelta:P}=o,c=10,k=m-c-10,y=u.flatMap(l=>l.data.map(i=>i[1])),E=u.flatMap(l=>l.data.map(i=>i[0])),p=Y!==void 0?Y:Math.min(...y),e=n!==void 0?n:Math.max(...y),r=e-p||1,C=Math.min(...E),L=Math.max(...E)-C||1;if(t.clearRect(0,0,a,m),x){t.beginPath(),t.strokeStyle="#aaa",t.lineWidth=1,t.font="15px sans-serif",t.fillStyle=u[0]?.strokeColor||"#000";for(let l=0;l<=w;l++){let i=p+r*l/w,T=c+(1-(i-p)/r)*k;t.moveTo(0,T),t.lineTo(5,T),t.fillText(i.toFixed(2),8,T+5)}t.stroke()}for(let l of u){let i=l.data,T=l.strokeColor||"#000";t.beginPath(),t.strokeStyle=T,t.lineWidth=1;let g=!1,d=null,F=P||600;for(let b=0;be){g=!1,d=null;continue}d!=null&&v-d>F&&(g=!1);let f=(v-C)/L*a,M=c+(1-(h-p)/r)*k;g?t.lineTo(f,M):(t.moveTo(f,M),g=!0),d=v}t.stroke()}if(s!==null&&s>=0&&s<=a){let l=C+s/a*L;t.beginPath(),t.strokeStyle="#888",t.lineWidth=1,t.moveTo(s,0),t.lineTo(s,m),t.stroke(),t.font="12px sans-serif";let i=15;t.fillStyle="#fff";let T=o.dateFormat??(g=>new Date(g).toLocaleTimeString());t.fillText(T(l),s+6,i),i+=15;for(let g of u){let{data:d,strokeColor:F="#fff"}=g,b=d[0],v=Math.abs(b[0]-l);for(let h=1;h{e.style.position="absolute",e.style.top=e.style.left="0",e.style.width=e.style.height="100%",o.appendChild(e)}),m.style.pointerEvents="none";let Y=a.getContext("2d"),n=m.getContext("2d"),x=t,w={...u},s=null,P=!1,c=()=>{let e=o.clientWidth,r=o.clientHeight;!e||!r||a.width===e&&a.height===r||(a.width=e,a.height=r,m.width=e,m.height=r,k(),y(s))},R=typeof ResizeObserver<"u"?new ResizeObserver(c):null;R?.observe(o),window.addEventListener("resize",c);let k=()=>{N(Y,x,{...w,width:a.width,height:a.height})},y=e=>{if(n.clearRect(0,0,m.width,m.height),e==null)return;let r=x.flatMap(f=>f.data.map(M=>M[1])),C=x.flatMap(f=>f.data.map(M=>M[0])),z=w.min??Math.min(...r),l=(w.max??Math.max(...r))-z||1,i=Math.min(...C),g=Math.max(...C)-i||1,d=i+e/a.width*g;n.beginPath(),n.strokeStyle="#888",n.lineWidth=1,n.moveTo(e,0),n.lineTo(e,m.height),n.stroke(),n.font="12px sans-serif",n.fillStyle="#fff";let F=w.dateFormat??(f=>new Date(f).toLocaleTimeString());n.fillText(F(d),e>a.width-100?e-100:e+6,15);let b=10,h=m.height-b-10,S=30;for(let{data:f,strokeColor:M="#fff"}of x){let D=f[0],O=Math.abs(D[0]-d);for(let A=1;Aa.width-100?e-100:e+6,S),S+=15;let W=b+(1-(D[1]-z)/l)*h;n.beginPath(),n.arc(e,W,3,0,Math.PI*2),n.fill()}},E=e=>{let r=o.getBoundingClientRect(),C=a.width/r.width;s=(e.clientX-r.left)*C,P||(P=!0,requestAnimationFrame(()=>{y(s),P=!1}))},p=()=>{s=null,y(null)};return o.addEventListener("mousemove",E),o.addEventListener("mouseleave",p),setTimeout(c,0),{element:o,setSeries(e){x=e,c(),k(),y(s)},setOptions(e){w={...w,...e},c(),k(),y(s)},destroy(){R?.disconnect(),window.removeEventListener("resize",c),o.removeEventListener("mousemove",E),o.removeEventListener("mouseleave",p)}}}export{X as createChartElement,N as drawChart}; diff --git a/package.json b/package.json index d7c72fa..1efff43 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "publishConfig": { "access": "public" }, - "version": "1.0.6", + "version": "1.0.7", "description": "Lightweight charting library for the browser and Node.js", "license": "MIT", "repository": { @@ -41,7 +41,6 @@ "serve": "npx http-server . -c-1 --gzip" }, "dependencies": { - "axios": "^1.5.0" }, "devDependencies": { "esbuild": "^0.25.5", diff --git a/src/index.ts b/src/index.ts index f74e303..e68895e 100755 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,8 @@ export function drawChart( yTicks?: number; backgroundColor?: string; cursorX?: number | null; + maxTimeDelta?: number | null; + dateFormat?: (ts: number) => string; } ): void { const { @@ -24,7 +26,8 @@ export function drawChart( max, showYAxis = false, yTicks = 5, - cursorX = null + cursorX = null, + maxTimeDelta } = opts; const topPadding = 10; @@ -67,10 +70,12 @@ export function drawChart( let penDown = false; let lastT: number | null = null; - const MAX_TIME_DELTA = 600; + // let MAX_TIME_DELTA = 600; + let max_time_delta = maxTimeDelta || 600; for (let i = 0; i < data.length; i++) { const [ti, vi] = data[i]; + const invalid = ti == null || vi == null || @@ -85,7 +90,7 @@ export function drawChart( lastT = null; continue; } - if (lastT != null && ti - lastT > MAX_TIME_DELTA) penDown = false; + if (lastT != null && ti - lastT > max_time_delta) penDown = false; const x = ((ti - minX) / rangeX) * width; const y = topPadding + (1 - (vi - minVal) / rangeY) * usableHeight; if (!penDown) { @@ -111,7 +116,8 @@ export function drawChart( ctx.font = '12px sans-serif'; let textY = 15; ctx.fillStyle = '#fff'; - ctx.fillText(new Date(tAtCursor).toLocaleTimeString(), cursorX + 6, textY); + const formatTime = opts.dateFormat ?? ((ts: number) => new Date(ts).toLocaleTimeString()); + ctx.fillText(formatTime(tAtCursor), cursorX + 6, textY); textY += 15; for (const series of seriesList) { @@ -142,6 +148,8 @@ export function createChartElement( showYAxis?: boolean; yTicks?: number; backgroundColor?: string; + maxTimeDelta?: number | null; + dateFormat?: (ts: number) => string; } ) { const container = document.createElement('div'); @@ -217,7 +225,8 @@ export function createChartElement( overlayCtx.font = '12px sans-serif'; overlayCtx.fillStyle = '#fff'; - overlayCtx.fillText(new Date(tAtCursor).toLocaleTimeString(), cursorX > baseCanvas.width - 100 ? cursorX - 100 : cursorX + 6, 15); + const formatTime = currentOpts.dateFormat ?? ((ts: number) => new Date(ts).toLocaleTimeString()); + overlayCtx.fillText(formatTime(tAtCursor), cursorX > baseCanvas.width - 100 ? cursorX - 100 : cursorX + 6, 15); const topPadding = 10; const bottomPadding = 10; diff --git a/test.html b/test.html index 8be8b45..24d784b 100644 --- a/test.html +++ b/test.html @@ -16,8 +16,18 @@ import { createChartElement } from './dist/uchart.js'; const container = document.getElementById('chartContainer'); - const chart = createChartElement([], { showYAxis:true, yTicks:5 }); - container.appendChild(chart.element); + const chart = createChartElement([], { + showYAxis: true, + yTicks: 5, + dateFormat: (ts) => new Date(ts).toLocaleString('cs-CZ', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + day: '2-digit', + month: '2-digit', + year: 'numeric' + }) + }); container.appendChild(chart.element); let phase = 0;