Compare commits
13 Commits
7fdefba622
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5035993825 | ||
|
|
14adfa867e | ||
|
|
819ecc5e7c | ||
| 11aedd600f | |||
| 60d505935b | |||
| 0093eda1d6 | |||
| c6c8277c49 | |||
| 864ed9a5d2 | |||
| 193d073f85 | |||
| 474865ddb4 | |||
| ac387674a4 | |||
| a703a042e1 | |||
| c6e02087c8 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
src/index.js
|
|
||||||
|
|||||||
113
README.md
113
README.md
@@ -1,16 +1,45 @@
|
|||||||
# uChart
|
# µChart
|
||||||
|
|
||||||
Lightweight, canvas‑based charting library written in TypeScript, designed for real‑time data streams and small bundle size.
|
Lightweight, canvas‑based charting library written in TypeScript, designed for real‑time data streams and small bundle size.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 💡 **Zero external rendering deps** – plain `<canvas>`
|
- 💡 **Zero external deps** – plain `<canvas>`
|
||||||
- ⚡ **Real‑time friendly** – redraws only what’s needed
|
- ⚡ **Real‑time friendly** – redraws only what’s needed
|
||||||
- 📦 **ESM ready** – authored as native ES modules
|
- 📦 **ESM ready** – authored as native ES modules
|
||||||
- 📝 **TypeScript types** included (`*.d.ts`)
|
- 📝 **TypeScript types** included (`*.d.ts`)
|
||||||
- 🎛️ Configurable axes, ticks, colors & cursor read‑outs
|
- 🎛️ 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 |
|
||||||
|
| `averageData` | `number` | Compute simple moving average of lasy n data to smooth out line (optional) |
|
||||||
|
|
||||||
|
```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
|
## Installation
|
||||||
|
|
||||||
@@ -25,47 +54,61 @@ npm install @meteolab/uchart
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>uChart Interactive Test</title>
|
<title>uChart Interactive Test</title>
|
||||||
<style>
|
<style>
|
||||||
#chartContainer{width:1000px;height:400px;margin:20px auto;background:#111}
|
#chartContainer { width: 1000px; height: 400px; margin: 20px auto; background:#111; }
|
||||||
body{background:#222}
|
body { background:#222; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="chartContainer"></div>
|
<div id="chartContainer"></div>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { createChartElement } from './dist/uchart.js';
|
import { createChartElement } from './dist/uchart.js';
|
||||||
|
|
||||||
const container = document.getElementById('chartContainer');
|
const container = document.getElementById('chartContainer');
|
||||||
const chart = createChartElement([], { showYAxis:true, yTicks:5 });
|
const chart = createChartElement([], {
|
||||||
container.appendChild(chart.element);
|
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;
|
let phase = 0;
|
||||||
|
|
||||||
function sine(len, shift, amp, off, f=1){
|
function generateSineSeries(len, shift, amp, off, freq = 1){
|
||||||
const now=Date.now(), d=[];
|
const now = Date.now();
|
||||||
for(let i=0;i<len;i++){
|
const data=[];
|
||||||
const t=now+i*10, x=i/len*2*Math.PI*f, y=Math.sin(x+shift)*amp+off;
|
for(let i=0;i<len;i++){
|
||||||
d.push([t,y]);
|
const t = now + i*10;
|
||||||
|
const x = i/len*2*Math.PI*freq;
|
||||||
|
const y = Math.sin(x+shift)*amp + off;
|
||||||
|
data.push([t,y]);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
return d;
|
|
||||||
}
|
|
||||||
|
|
||||||
function update(){
|
function update(){
|
||||||
chart.setSeries([
|
chart.setSeries([
|
||||||
{ data: sine(1500, phase, 20, 10, 1), strokeColor:'#0f0' },
|
{ data: generateSineSeries(1500, phase, 20, 10, 1), strokeColor:'#0f0' },
|
||||||
{ data: sine(1500, phase, 20, 10, 2), strokeColor:'#f00' },
|
{ data: generateSineSeries(1500, phase, 20, 10, 2), strokeColor:'#f00' },
|
||||||
{ data: sine(1500, phase, 20, 10, 0.5), strokeColor:'#00f' }
|
{ data: generateSineSeries(1500, phase, 20, 10, 0.5), strokeColor:'#00f' }
|
||||||
]);
|
]);
|
||||||
phase += 0.01;
|
phase += 0.01;
|
||||||
}
|
}
|
||||||
update();
|
update()
|
||||||
setInterval(update, 50);
|
setInterval(update, 50);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Test
|
## Test
|
||||||
|
|||||||
33
dist/index.d.ts
vendored
Normal file
33
dist/index.d.ts
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
export type ChartSeries = {
|
||||||
|
data: [number, number][];
|
||||||
|
strokeColor?: string;
|
||||||
|
};
|
||||||
|
export declare function drawChart(ctx: CanvasRenderingContext2D, seriesList: ChartSeries[], opts: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
showYAxis?: boolean;
|
||||||
|
yTicks?: number;
|
||||||
|
backgroundColor?: string;
|
||||||
|
cursorX?: number | null;
|
||||||
|
maxTimeDelta?: number | null;
|
||||||
|
dateFormat?: (ts: number) => string;
|
||||||
|
averageData?: number | null;
|
||||||
|
}): void;
|
||||||
|
export declare function createChartElement(seriesList: ChartSeries[], opts: {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
showYAxis?: boolean;
|
||||||
|
yTicks?: number;
|
||||||
|
backgroundColor?: string;
|
||||||
|
maxTimeDelta?: number | null;
|
||||||
|
dateFormat?: (ts: number) => string;
|
||||||
|
}): {
|
||||||
|
element: HTMLDivElement;
|
||||||
|
setSeries(series: ChartSeries[]): void;
|
||||||
|
setOptions(o: Partial<typeof opts>): void;
|
||||||
|
destroy(): void;
|
||||||
|
};
|
||||||
2
dist/uchart.js
vendored
2
dist/uchart.js
vendored
@@ -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;v<i.length;v++){let[h,f]=i[v];if(h==null||f==null||isNaN(h)||isNaN(f)||!isFinite(h)||!isFinite(f)||f<p||f>P){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<g.length;h++){let f=Math.abs(g[h][0]-s);f<v&&(v=f,b=g[h])}t.fillStyle=C,t.fillText(b[1].toFixed(2),l+6,i),i+=15}}}function X(t,m){let a=document.createElement("div");a.style.position="relative",a.style.width=m.width!==void 0?`${m.width}px`:"100%",a.style.height=m.height!==void 0?`${m.height}px`:"100%";let o=document.createElement("canvas"),d=document.createElement("canvas");[o,d].forEach(e=>{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;R<u.length;R++){let O=Math.abs(u[R][0]-b);O<D&&(D=O,L=u[R])}n.fillStyle=M,n.fillText(L[1].toFixed(2),e>o.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 X(e,r=5){if(e.length<r)return e;let o=[];for(let n=0;n<e.length;n++){let i=0,x=0;for(let a=Math.max(0,n-Math.floor(r/2));a<=Math.min(e.length-1,n+Math.floor(r/2));a++)i+=e[a][1],x++;o.push([e[n][0],i/x])}return o}function z(e,r,o){let{width:n,height:i,min:x,max:a,showYAxis:k=!1,yTicks:y=5,cursorX:m=null,maxTimeDelta:E}=o,c=10,S=i-c-10,w=r.flatMap(s=>s.data.map(l=>l[1])),D=r.flatMap(s=>s.data.map(l=>l[0])),p=x!==void 0?x:Math.min(...w),t=a!==void 0?a:Math.max(...w),h=t-p||1,C=Math.min(...D),O=Math.max(...D)-C||1;if(e.clearRect(0,0,n,i),k){e.beginPath(),e.strokeStyle="#aaa",e.lineWidth=1,e.font="15px sans-serif",e.fillStyle=r[0]?.strokeColor||"#000";for(let s=0;s<=y;s++){let l=p+h*s/y,M=c+(1-(l-p)/h)*S;e.moveTo(0,M),e.lineTo(5,M),e.fillText(l.toFixed(2),8,M+5)}e.stroke()}for(let s of r){let l=X(s.data,o.averageData??1),M=s.strokeColor||"#000";e.beginPath(),e.strokeStyle=M,e.lineWidth=1;let g=!1,d=null,F=E||600;for(let b=0;b<l.length;b++){let[v,u]=l[b];if(v==null||u==null||isNaN(v)||isNaN(u)||!isFinite(v)||!isFinite(u)||u<p||u>t){g=!1,d=null;continue}d!=null&&v-d>F&&(g=!1);let f=(v-C)/O*n,T=c+(1-(u-p)/h)*S;g?e.lineTo(f,T):(e.moveTo(f,T),g=!0),d=v}e.stroke()}if(m!==null&&m>=0&&m<=n){let s=C+m/n*O;e.beginPath(),e.strokeStyle="#888",e.lineWidth=1,e.moveTo(m,0),e.lineTo(m,i),e.stroke(),e.font="12px sans-serif";let l=15;e.fillStyle="#fff";let M=o.dateFormat??(g=>new Date(g).toLocaleTimeString());e.fillText(M(s),m+6,l),l+=15;for(let g of r){let{data:d,strokeColor:F="#fff"}=g,b=d[0],v=Math.abs(b[0]-s);for(let u=1;u<d.length;u++){let P=Math.abs(d[u][0]-s);P<v&&(v=P,b=d[u])}e.fillStyle=F,e.fillText(b[1].toFixed(2),m+6,l),l+=15}}}function H(e,r){let o=document.createElement("div");o.style.position="relative",o.style.width=r.width!==void 0?`${r.width}px`:"100%",o.style.height=r.height!==void 0?`${r.height}px`:"100%";let n=document.createElement("canvas"),i=document.createElement("canvas");[n,i].forEach(t=>{t.style.position="absolute",t.style.top=t.style.left="0",t.style.width=t.style.height="100%",o.appendChild(t)}),i.style.pointerEvents="none";let x=n.getContext("2d"),a=i.getContext("2d"),k=e,y={...r},m=null,E=!1,c=()=>{let t=o.clientWidth,h=o.clientHeight;!t||!h||n.width===t&&n.height===h||(n.width=t,n.height=h,i.width=t,i.height=h,S(),w(m))},R=typeof ResizeObserver<"u"?new ResizeObserver(c):null;R?.observe(o),window.addEventListener("resize",c);let S=()=>{z(x,k,{...y,width:n.width,height:n.height})},w=t=>{if(a.clearRect(0,0,i.width,i.height),t==null)return;let h=k.flatMap(f=>f.data.map(T=>T[1])),C=k.flatMap(f=>f.data.map(T=>T[0])),L=y.min??Math.min(...h),s=(y.max??Math.max(...h))-L||1,l=Math.min(...C),g=Math.max(...C)-l||1,d=l+t/n.width*g;a.beginPath(),a.strokeStyle="#888",a.lineWidth=1,a.moveTo(t,0),a.lineTo(t,i.height),a.stroke(),a.font="12px sans-serif",a.fillStyle="#fff";let F=y.dateFormat??(f=>new Date(f).toLocaleTimeString());a.fillText(F(d),t>n.width-100?t-100:t+6,15);let b=10,u=i.height-b-10,P=30;for(let{data:f,strokeColor:T="#fff"}of k){let Y=f[0],V=Math.abs(Y[0]-d);for(let A=1;A<f.length;A++){let W=Math.abs(f[A][0]-d);W<V&&(V=W,Y=f[A])}a.fillStyle=T,a.fillText(Y[1].toFixed(2),t>n.width-100?t-100:t+6,P),P+=15;let N=b+(1-(Y[1]-L)/s)*u;a.beginPath(),a.arc(t,N,3,0,Math.PI*2),a.fill()}},D=t=>{let h=o.getBoundingClientRect(),C=n.width/h.width;m=(t.clientX-h.left)*C,E||(E=!0,requestAnimationFrame(()=>{w(m),E=!1}))},p=()=>{m=null,w(null)};return o.addEventListener("mousemove",D),o.addEventListener("mouseleave",p),setTimeout(c,0),{element:o,setSeries(t){k=t,c(),S(),w(m)},setOptions(t){y={...y,...t},c(),S(),w(m)},destroy(){R?.disconnect(),window.removeEventListener("resize",c),o.removeEventListener("mousemove",D),o.removeEventListener("mouseleave",p)}}}export{H as createChartElement,z as drawChart};
|
||||||
|
|||||||
30
package.json
30
package.json
@@ -1,23 +1,33 @@
|
|||||||
{
|
{
|
||||||
"name": "@meteolab/uchart",
|
"name": "@meteolab/uchart",
|
||||||
"publishConfig": { "access": "public" },
|
"publishConfig": {
|
||||||
"version": "1.0.3",
|
"access": "public"
|
||||||
|
},
|
||||||
|
"version": "1.0.11",
|
||||||
"description": "Lightweight charting library for the browser and Node.js",
|
"description": "Lightweight charting library for the browser and Node.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://git.meteolab.online/mi/uChart"
|
"url": "https://git.mipem.co/mi/uChart"
|
||||||
},
|
},
|
||||||
"keywords": ["chart", "visualization", "typescript", "esbuild"],
|
"keywords": [
|
||||||
|
"chart",
|
||||||
|
"visualization",
|
||||||
|
"typescript",
|
||||||
|
"esbuild"
|
||||||
|
],
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": ["dist"],
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
"main": "./dist/uchart.js",
|
"main": "./dist/uchart.js",
|
||||||
"module": "./dist/uchart.js",
|
"module": "./dist/uchart.js",
|
||||||
"types": "./dist/uchart.d.ts",
|
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/uchart.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"import": "./dist/uchart.js",
|
"import": "./dist/uchart.js",
|
||||||
"require": "./dist/uchart.js"
|
"require": "./dist/uchart.js"
|
||||||
}
|
}
|
||||||
@@ -25,14 +35,12 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rm -rf dist",
|
"clean": "rm -rf dist",
|
||||||
"build:js": "esbuild src/index.ts --bundle --minify --format=esm --outfile=dist/uchart.js",
|
"build:js": "esbuild src/index.ts --bundle --minify --format=esm --outfile=dist/uchart.js",
|
||||||
"build:types": "tsc -p tsconfig.json",
|
"build:types": "tsc --declaration --emitDeclarationOnly",
|
||||||
"build": "npm run clean && npm run build:js && npm run build:types",
|
"build": "npm run clean && npm run build:js && npm run build:types",
|
||||||
"prepublishOnly": "npm run build",
|
"prepublishOnly": "npm run build",
|
||||||
"serve": "npx http-server . -c-1 --gzip"
|
"serve": "npx http-server . -c-1 --gzip"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {},
|
||||||
"axios": "^1.5.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.25.5",
|
"esbuild": "^0.25.5",
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
|
|||||||
37
src/index.ts
37
src/index.ts
@@ -3,6 +3,21 @@ export type ChartSeries = {
|
|||||||
strokeColor?: string;
|
strokeColor?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function averageData(data: [number, number][], windowSize: number = 5): [number, number][] {
|
||||||
|
if (data.length < windowSize) return data;
|
||||||
|
const result: [number, number][] = [];
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
let sum = 0;
|
||||||
|
let count = 0;
|
||||||
|
for (let j = Math.max(0, i - Math.floor(windowSize / 2)); j <= Math.min(data.length - 1, i + Math.floor(windowSize / 2)); j++) {
|
||||||
|
sum += data[j][1];
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
result.push([data[i][0], sum / count]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export function drawChart(
|
export function drawChart(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
seriesList: ChartSeries[],
|
seriesList: ChartSeries[],
|
||||||
@@ -15,6 +30,9 @@ export function drawChart(
|
|||||||
yTicks?: number;
|
yTicks?: number;
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
cursorX?: number | null;
|
cursorX?: number | null;
|
||||||
|
maxTimeDelta?: number | null;
|
||||||
|
dateFormat?: (ts: number) => string;
|
||||||
|
averageData?:number | null;
|
||||||
}
|
}
|
||||||
): void {
|
): void {
|
||||||
const {
|
const {
|
||||||
@@ -24,7 +42,8 @@ export function drawChart(
|
|||||||
max,
|
max,
|
||||||
showYAxis = false,
|
showYAxis = false,
|
||||||
yTicks = 5,
|
yTicks = 5,
|
||||||
cursorX = null
|
cursorX = null,
|
||||||
|
maxTimeDelta
|
||||||
} = opts;
|
} = opts;
|
||||||
|
|
||||||
const topPadding = 10;
|
const topPadding = 10;
|
||||||
@@ -59,7 +78,7 @@ export function drawChart(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const series of seriesList) {
|
for (const series of seriesList) {
|
||||||
const data = series.data;
|
const data = averageData(series.data, opts.averageData ?? 1);
|
||||||
const color = series.strokeColor || '#000';
|
const color = series.strokeColor || '#000';
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.strokeStyle = color;
|
ctx.strokeStyle = color;
|
||||||
@@ -67,10 +86,12 @@ export function drawChart(
|
|||||||
|
|
||||||
let penDown = false;
|
let penDown = false;
|
||||||
let lastT: number | null = null;
|
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++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
const [ti, vi] = data[i];
|
const [ti, vi] = data[i];
|
||||||
|
|
||||||
const invalid =
|
const invalid =
|
||||||
ti == null ||
|
ti == null ||
|
||||||
vi == null ||
|
vi == null ||
|
||||||
@@ -85,7 +106,7 @@ export function drawChart(
|
|||||||
lastT = null;
|
lastT = null;
|
||||||
continue;
|
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 x = ((ti - minX) / rangeX) * width;
|
||||||
const y = topPadding + (1 - (vi - minVal) / rangeY) * usableHeight;
|
const y = topPadding + (1 - (vi - minVal) / rangeY) * usableHeight;
|
||||||
if (!penDown) {
|
if (!penDown) {
|
||||||
@@ -111,7 +132,8 @@ export function drawChart(
|
|||||||
ctx.font = '12px sans-serif';
|
ctx.font = '12px sans-serif';
|
||||||
let textY = 15;
|
let textY = 15;
|
||||||
ctx.fillStyle = '#fff';
|
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;
|
textY += 15;
|
||||||
|
|
||||||
for (const series of seriesList) {
|
for (const series of seriesList) {
|
||||||
@@ -142,6 +164,8 @@ export function createChartElement(
|
|||||||
showYAxis?: boolean;
|
showYAxis?: boolean;
|
||||||
yTicks?: number;
|
yTicks?: number;
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
|
maxTimeDelta?: number | null;
|
||||||
|
dateFormat?: (ts: number) => string;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
@@ -217,7 +241,8 @@ export function createChartElement(
|
|||||||
|
|
||||||
overlayCtx.font = '12px sans-serif';
|
overlayCtx.font = '12px sans-serif';
|
||||||
overlayCtx.fillStyle = '#fff';
|
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 topPadding = 10;
|
||||||
const bottomPadding = 10;
|
const bottomPadding = 10;
|
||||||
|
|||||||
14
test.html
14
test.html
@@ -16,8 +16,18 @@
|
|||||||
import { createChartElement } from './dist/uchart.js';
|
import { createChartElement } from './dist/uchart.js';
|
||||||
|
|
||||||
const container = document.getElementById('chartContainer');
|
const container = document.getElementById('chartContainer');
|
||||||
const chart = createChartElement([], { showYAxis:true, yTicks:5 });
|
const chart = createChartElement([], {
|
||||||
container.appendChild(chart.element);
|
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;
|
let phase = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES6",
|
"target": "ES6",
|
||||||
"module": "ES6",
|
"module": "ES6",
|
||||||
|
"declaration": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true,
|
||||||
|
"outDir": "./dist"
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user