This is an automated email from the ASF dual-hosted git repository.

ovilia pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/echarts-custom-series.git


The following commit(s) were added to refs/heads/main by this push:
     new eb08a50  feat(stage): complete stage chart
eb08a50 is described below

commit eb08a502b6cb48222b0b1099a159e12b2a209048
Author: Ovilia <[email protected]>
AuthorDate: Sun Sep 29 17:07:00 2024 +0800

    feat(stage): complete stage chart
---
 custom-series/stage/src/index.ts    | 157 ++++++++++++++++++++++++------------
 custom-series/stage/test/index.html |  52 +++++++++---
 2 files changed, 144 insertions(+), 65 deletions(-)

diff --git a/custom-series/stage/src/index.ts b/custom-series/stage/src/index.ts
index 13e6e61..71eb248 100644
--- a/custom-series/stage/src/index.ts
+++ b/custom-series/stage/src/index.ts
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-import echarts from 'echarts';
+import echarts, { zrUtil } from 'echarts';
 import type {
   CustomElementOption,
   CustomRootElementOption,
@@ -25,12 +25,31 @@ import type {
 } from 'echarts/types/src/chart/custom/CustomSeries.d.ts';
 import type { EChartsExtensionInstallRegisters } from 
'echarts/src/extension.ts';
 
-type Envelope = {
+interface Envelope {
   show?: boolean;
   color?: 'auto' | string;
+  externalRadius?: number;
   opacity?: number;
   margin?: number;
-};
+  dpr?: number;
+}
+
+interface ItemStyle {
+  borderRadius?: number;
+  verticalMargin?: number;
+  minHorizontalSize?: number;
+}
+
+interface AxisLabel {
+  color?: string;
+  formatter?: (value: string, index: number) => string;
+}
+
+interface StageItemPayload {
+  itemStyle?: ItemStyle;
+  axisLabel?: AxisLabel;
+  envelope?: Envelope;
+}
 
 const renderItem = (
   params: echarts.CustomSeriesRenderItemParams,
@@ -46,10 +65,22 @@ const renderItem = (
   const bandWidth = api.coord([0, 0])[1] - api.coord([0, 1])[1];
   const fontSize = 14;
   const textMargin = 5;
-  const barMargin = 8;
   const color = api.visual('color');
-  const borderRadius = (params.itemPayload.borderRadius as number) || 8;
-  const barMinHeight = 2;
+  const itemPayload = params.itemPayload as StageItemPayload;
+  const itemStyle = itemPayload.itemStyle || {};
+  const borderRadius = itemStyle.borderRadius || 8;
+  const externalRadius = zrUtil.retrieve2(
+    itemPayload.envelope?.externalRadius,
+    6
+  ) as number;
+  const barVerticalMargin = zrUtil.retrieve2(
+    itemStyle.verticalMargin,
+    8
+  ) as number;
+  const barMinWidth = zrUtil.retrieve2(
+    itemStyle.minHorizontalSize,
+    3
+  ) as number;
 
   const children: CustomElementOption[] = [];
   const boxes: { x: number; y: number; width: number; height: number }[] =
@@ -61,12 +92,13 @@ const renderItem = (
     }[]) || [];
 
   const span = endCoord[0] - startCoord[0];
-  const height = Math.max(span, barMinHeight);
+  const height = Math.max(span, barMinWidth);
   const shape = {
     x: startCoord[0] - (height - span) / 2,
-    y: startCoord[1] - bandWidth / 2 + textMargin + fontSize + barMargin,
+    y:
+      startCoord[1] - bandWidth / 2 + textMargin + fontSize + 
barVerticalMargin,
     width: height,
-    height: bandWidth - fontSize - textMargin - 2 * barMargin,
+    height: bandWidth - fontSize - textMargin - 2 * barVerticalMargin,
   };
   children.push({
     type: 'rect',
@@ -89,14 +121,19 @@ const renderItem = (
   }
   const renderedStages = params.context.renderedStages as boolean[];
   if (!renderedStages[stageIndex]) {
+    const axisLabel: AxisLabel = itemPayload.axisLabel || {};
+    let text = api.ordinalRawValue(2) as string;
+    if (typeof axisLabel.formatter === 'function') {
+      text = axisLabel.formatter(text, stageIndex as number);
+    }
     // Each stage only render once as axis label
     children.push({
       type: 'text',
       style: {
         x: (params.coordSys as any).x + textMargin,
         y: startCoord[1] - bandWidth / 2 + textMargin + fontSize,
-        fill: (params.itemPayload.axisLabelColor as string) || '#777',
-        text: api.ordinalRawValue(2) as string,
+        fill: axisLabel.color || '#8A8A8A',
+        text,
         verticalAlign: 'bottom',
       },
     });
@@ -107,51 +144,58 @@ const renderItem = (
   if (params.dataIndex === params.dataInsideLength - 1) {
     const allColors: string[] = [];
     for (let i = 0; i < params.dataInsideLength; i++) {
-      allColors.push(api.visual('color', i) as string);
+      const color = api.visual('color', i) as string;
+      if (allColors.indexOf(color) < 0) {
+        allColors.push(color);
+      }
     }
 
-    const envelope: Envelope = params.itemPayload.envelope || {};
+    const envelope: Envelope = itemPayload.envelope || {};
     if (envelope.show !== false && boxes.length > 1) {
-      const margin = echarts.zrUtil.retrieve2(envelope.margin as number, 5);
+      const margin = echarts.zrUtil.retrieve2(envelope.margin as number, 2);
 
       // Sort boxes by x, then by y
       boxes.sort((a, b) => a.x - b.x || a.y - b.y);
-      console.log(boxes);
 
-      const canvas = document.createElement('canvas');
       const coordSys = params.coordSys as any;
-      const dpr = window.devicePixelRatio || 1;
+      const dpr = envelope.dpr == null ? 2 : envelope.dpr || 1;
       const canvasWidth = coordSys.width * dpr;
       const canvasHeight = coordSys.height * dpr;
-      canvas.width = canvasWidth;
-      canvas.height = canvasHeight;
+      const canvas = createCanvas(canvasWidth, canvasHeight);
+      const ox = coordSys.x;
+      const oy = coordSys.y;
 
       const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
-
       if (allColors.length > 0 && !envelope.color) {
         const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeight);
         for (let i = 0; i < allColors.length; i++) {
-          gradient.addColorStop(i / (allColors.length - 1), allColors[i]);
+          // For example, if there are 4 colors, the gradient stops are 1/8,
+          // 3/8, 5/8, 7/8.
+          gradient.addColorStop(
+            (i * 2 + 1) / (allColors.length * 2),
+            allColors[i]
+          );
         }
         ctx.fillStyle = gradient;
       } else {
         ctx.fillStyle = envelope.color || '#888';
       }
+      const opacity = zrUtil.retrieve2(envelope.opacity as number, 0.25);
 
       for (let i = 0; i < boxes.length; i++) {
         const box = boxes[i];
 
-        ctx.fillStyle = '#888';
         drawRoundedRect(
           ctx,
-          (box.x - margin - coordSys.x) * dpr,
-          (box.y - margin - coordSys.y) * dpr,
+          (box.x - margin - ox) * dpr,
+          (box.y - margin - oy) * dpr,
           (box.width + margin * 2) * dpr,
           (box.height + margin * 2) * dpr,
           (Math.min(borderRadius, box.width / 2) + margin) * dpr
         );
 
         if (i > 0) {
+          ctx.beginPath();
           const prevBox = boxes[i - 1];
           const isPrevLower = prevBox.y > box.y + box.height;
           const height = isPrevLower
@@ -161,18 +205,19 @@ const renderItem = (
             ? box.y + box.height - borderRadius
             : prevBox.y + prevBox.height - borderRadius;
 
+          if (box.x - margin >= prevBox.x + prevBox.width + margin) {
+            // No overlapping
+            continue;
+          }
+
           // Draw outer border-radius
-          ctx.beginPath();
           if (isPrevLower) {
-            ctx.fillStyle = '#f00';
             if (box.x - margin - prevBox.x > 0) {
-              const right = Math.ceil((box.x - margin - coordSys.x) * dpr);
-              const bottom = (prevBox.y - margin - coordSys.y) * dpr;
+              const right = Math.ceil((box.x - margin - ox) * dpr);
+              const bottom = (prevBox.y - margin - oy) * dpr;
               const r =
-                Math.min(
-                  (box.x - margin - prevBox.x) / 2,
-                  margin + borderRadius
-                ) * dpr;
+                Math.min((box.x - margin - prevBox.x) / 2, externalRadius) *
+                dpr;
               ctx.moveTo(right, bottom + r);
               ctx.arc(right - r, bottom - r, r, 0, Math.PI / 2);
               ctx.lineTo(right, bottom + margin * dpr);
@@ -180,14 +225,14 @@ const renderItem = (
             }
 
             if (box.x + box.width - prevBox.x - prevBox.width - margin > 0) {
-              const top = (box.y + box.height + margin - coordSys.y) * dpr;
+              const top = (box.y + box.height + margin - oy) * dpr;
               const left = Math.floor(
-                (prevBox.x + prevBox.width + margin - coordSys.x) * dpr
+                (prevBox.x + prevBox.width + margin - ox) * dpr
               );
               const r =
                 Math.min(
                   (box.x + box.width - prevBox.x - prevBox.width - margin) / 2,
-                  margin + borderRadius
+                  externalRadius
                 ) * dpr;
               ctx.moveTo(left, top + r);
               ctx.arc(left + r, top + r, r, Math.PI, Math.PI * 1.5);
@@ -195,16 +240,12 @@ const renderItem = (
               ctx.lineTo(left, top);
             }
           } else {
-            ctx.fillStyle = '#0f0';
             if (box.x - margin - prevBox.x > 0) {
-              const right = Math.ceil((box.x - margin - coordSys.x) * dpr);
-              const top =
-                (prevBox.y + prevBox.height + margin - coordSys.y) * dpr;
+              const right = Math.ceil((box.x - margin - ox) * dpr);
+              const top = (prevBox.y + prevBox.height + margin - oy) * dpr;
               const r =
-                Math.min(
-                  (box.x - margin - prevBox.x) / 2,
-                  margin + borderRadius
-                ) * dpr;
+                Math.min((box.x - margin - prevBox.x) / 2, externalRadius) *
+                dpr;
               ctx.moveTo(right, top + r);
               ctx.arc(right - r, top + r, r, -Math.PI / 2, 0);
               ctx.lineTo(right, top - margin * dpr);
@@ -212,19 +253,19 @@ const renderItem = (
             }
 
             if (box.x + box.width - prevBox.x - prevBox.width - margin > 0) {
-              const bottom = (box.y - margin - coordSys.y) * dpr;
+              const bottom = (box.y - margin - oy) * dpr;
               const left = Math.floor(
-                (prevBox.x + prevBox.width + margin - coordSys.x) * dpr
+                (prevBox.x + prevBox.width + margin - ox) * dpr
               );
               const r =
                 Math.min(
                   (box.x + box.width - prevBox.x - prevBox.width - margin) / 2,
-                  margin + borderRadius
+                  externalRadius
                 ) * dpr;
               ctx.moveTo(left + r, bottom);
               ctx.arc(left + r, bottom - r, r, Math.PI / 2, Math.PI);
-              ctx.lineTo(left, bottom + margin * dpr);
-              ctx.lineTo(left, bottom);
+              ctx.lineTo(left, bottom + (margin + borderRadius) * dpr);
+              ctx.lineTo(left + r, bottom);
             }
           }
           ctx.closePath();
@@ -232,8 +273,8 @@ const renderItem = (
 
           // Draw bars between boxes
           ctx.fillRect(
-            (prevBox.x + prevBox.width + margin - coordSys.x) * dpr,
-            (y - coordSys.y) * dpr,
+            (prevBox.x + prevBox.width + margin - ox) * dpr,
+            (y - oy) * dpr,
             (box.x - prevBox.x - prevBox.width - margin * 2) * dpr,
             height * dpr
           );
@@ -244,10 +285,13 @@ const renderItem = (
         type: 'image',
         style: {
           image: canvas,
-          x: coordSys.x,
-          y: coordSys.y,
-          opacity: 0.5,
+          x: coordSys.x * dpr,
+          y: coordSys.y * dpr,
+          opacity,
         },
+        silent: true,
+        scaleX: 1 / dpr,
+        scaleY: 1 / dpr,
       });
     }
   }
@@ -258,6 +302,13 @@ const renderItem = (
   } as CustomRootElementOption;
 };
 
+function createCanvas(width, height) {
+  const canvas = document.createElement('canvas');
+  canvas.width = width;
+  canvas.height = height;
+  return canvas;
+}
+
 function drawRoundedRect(
   ctx: CanvasRenderingContext2D,
   x: number,
diff --git a/custom-series/stage/test/index.html 
b/custom-series/stage/test/index.html
index 5fa9a85..7bfda34 100644
--- a/custom-series/stage/test/index.html
+++ b/custom-series/stage/test/index.html
@@ -14,9 +14,7 @@
     <script src="../dist/index.js"></script>
     <script>
         echarts.use(window.stageCustomSeriesInstaller);
-        const chart = echarts.init(document.getElementById('main'), null, {
-            renderer: 'svg'
-        });
+        const chart = echarts.init(document.getElementById('main'));
 
         const data = [
             [new Date('2024-09-07 06:12'), new Date('2024-09-07 06:12'), 
'Awake'],
@@ -35,14 +33,16 @@
             [new Date('2024-09-07 06:18'), new Date('2024-09-07 07:37'), 
'Core'],
             [new Date('2024-09-07 07:56'), new Date('2024-09-07 08:56'), 
'Core'],
             [new Date('2024-09-07 09:00'), new Date('2024-09-07 09:08'), 
'Core'],
-            [new Date('2024-09-07 09:29'), new Date('2024-09-07 10:01'), 
'Core'],
+            [new Date('2024-09-07 09:29'), new Date('2024-09-07 10:41'), 
'Core'],
             [new Date('2024-09-07 03:27'), new Date('2024-09-07 04:02'), 
'Deep'],
             [new Date('2024-09-07 04:36'), new Date('2024-09-07 04:40'), 
'Deep'],
             [new Date('2024-09-07 04:48'), new Date('2024-09-07 04:57'), 
'Deep'],
         ];
 
         function formatTime(time) {
-            return time.getHours() + ':' + time.getMinutes();
+            const minutes = time.getMinutes();
+            const minStr = minutes < 10 ? '0' + minutes : minutes;
+            return time.getHours() + ':' + minStr;
         }
 
         option = {
@@ -61,8 +61,18 @@
                         opacity: 0.8
                     }
                 },
-                min: value => value.min - 10 * 60 * 1000,
-                max: value => value.max + 10 * 60 * 1000
+                min: value => {
+                    // Max whole hour that is no biggeer than value
+                    return Math.floor(value.min / (60 * 60 * 1000)) * 60 * 60 
* 1000
+                },
+                max: value => {
+                    // Min whole hour that is no smaller than value
+                    return Math.ceil(value.max / (60 * 60 * 1000)) * 60 * 60 * 
1000
+                },
+                axisLabel: {
+                    align: 'left',
+                    color: '#c6c6c6'
+                }
             },
             yAxis: {
                 type: 'category',
@@ -74,7 +84,15 @@
                     show: false
                 },
                 axisLabel: {
-                    show: false
+                    show: false,
+                    // formatter: value => {
+                    //     return {
+                    //         Deep: '深度睡眠',
+                    //         REM: '快速动眼睡眠',
+                    //         Core: '核心睡眠',
+                    //         Awake: '清醒时间'
+                    //     }
+                    // }
                 },
                 axisLine: {
                     lineStyle: {
@@ -90,6 +108,16 @@
                 renderItem: 'stage',
                 colorBy: 'data',
                 itemPayload: {
+                    axisLabel: {
+                        formatter: value => {
+                            return {
+                                Deep: '深度睡眠',
+                                REM: '快速动眼睡眠',
+                                Core: '核心睡眠',
+                                Awake: '清醒时间'
+                            }[value]
+                        }
+                    },
                     envelope: {
                     }
                 },
@@ -106,10 +134,10 @@
                 dimension: 2,
                 inRange: {
                     color: {
-                        0: '#33379D',
-                        1: '#1395F4',
-                        2: '#59C7FD',
-                        3: '#FE816E'
+                        0: '#35349D',
+                        1: '#3478F6',
+                        2: '#59AAE1',
+                        3: '#EF8872'
                     }
                 },
                 seriesIndex: 0,


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to