Skip to main content

เมื่อต้องสร้างกราฟ Vital Signs ฉบับตามใจผู้ใช้ "จะเอาแบบนี้" แต่เหนื่อยคนทำ (ระบบ EMR)

Kongvut Sangkla

Intro

สวัสดีครับ บทความนี้น่าจะเป็นบทความแรกที่เล่าเกี่ยวกับงานที่ทำอยู่ที่ทำงาน ซึ่งเป็นช่วงที่ได้รับ Assigned ให้ทำระบบ EMR

EMR ถ้าจะให้อธิบายง่าย ๆ สั้น ๆ คือระบบที่เป็นเหมือนแฟ้มประวัติการรักษาคนไข้ ซึ่งภายในก็จะประกอบได้วยหลายส่วน (Sections) เช่น ผล Labs, Xray, Med Orders, Vital Signs Chart และอื่น ๆ ขอไม่ลงรายละเอียดลึก

โดยวันนี้จะมาพูดถึงแค่ Vital Signs Chart ซึ่งก็เป็นส่วนหนึ่งของ EMR และอันที่จริงก็จะไม่ลงรายละเอียดว่าคืออะไร เพราะจุดประสงค์ของบทความนี้ต้องการจะเล่าถึงวิธีทำ เพื่อให้ได้ตามที่ผู้ใช้งานต้องการ 😄

จะเอาแบบนี้

เมื่อคนสั่งงานส่งรูปภาพนี้มาให้ดู เป็นงานที่ต้องหยุดคิดเลยว่าจะทำยังไงดี เพราะผู้ใช้งานบอกว่า "อยากได้แบบนี้" อาจจะเป็นเพราะดูข้อมูลง่าย มองข้อมูลที่มีความสัมพันธ์กันในคอลั่มน์ตามวันและเวลา

แต่ในใจ ปัญหาแรกที่คิดคือ "จะทำยังไงดี ไม่เคยทำงานกราฟแนวนี้เลย" 😅

Vital Signs Chart

รูปภาพนี้ ไม่แน่ใจแหล่งที่มา น่าจะมาจาก HIS สักที่ที่ใช้ภายใน รพ.

วิเคราะห์และความท้าทาย

  1. ช่องช่วงเวลาชั่วโมง (คอลั่มน์) ของข้อมูลนั้น ๆ (ทั้งด้านบน ด้านล่าง) ต้องสัมพันธ์กัน
  2. มีทั้งการวาดเส้นของกราฟ และตารางข้อมูล
  3. ให้รองรับการดูทั้งบน PC (Desktop) และ มือถือ (Mobile) โดยที่ไม่ต้องย่อหรือลดขนาดกราฟแต่ให้สามารถ Scroll ซ้าย-ขวา ได้

ค้นคว้าหาข้อมูล

หลังจากที่ได้รับมอบหมายงาน อย่างแรกเลยเริ่มหาตัวช่วยก่อน

  1. ไปดูว่า HighCharts demos ว่ามีตัวอย่างไหนคล้าย ๆ แนวที่ต้องการบ้าง
  2. ถ้าจะสร้างตารางข้อมูลขึ้นมา แล้วเอารูปภาพของกราฟมาเป็นพื้นหลัง

สรุปได้ว่าทั้งตัวอย่างและแนวคิดที่ลองหาดูบน HighCharts ดูเหมือนจะไม่เวิร์คเพราะติดปัญหาคือ

  • แม้จะทำกราฟได้ แต่ตารางช่องช่วงเวลา (คอลั่มน์) ให้สัมพันธ์กันกับตารางข้อมูลด้านล่างคงทำได้ยาก
  • ข้อนี้สำคัญถ้าทำออกมา "คงไม่เหมือนที่ ผู้ใช้งานอยากได้"

ค้นพบไอเดีย

ในระหว่างที่หาข้อมูล และดูเหมือนสิ้นหวังก็ได้ไปพบ Canvas Web API ที่สามารถลากเส้น lineTo() จากนั้น stroke() เพื่อกำหนดเส้นตามตำแหน่ง x,y ได้

แนวคิดการวาดเส้นบน Canvas Web API

  • เริ่ม Path - beginPath()
  • ย้าย Point ไปที่จุดเริ่มต้น - moveTo()
  • ลากเส้นไปที่จุดที่ต้องการ - lineTo()
  • วาด Path - stroke()
const canvas = document.getElementById("myCanvas");
const ctx = canvas.getContext("2d");

ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(20, 100);
ctx.lineTo(70, 100);
ctx.stroke();

ทดลอง Demo Canvas Line stroke

ทดลอง PoC บน React

ทดลอง PoC บน React เพื่อพิสูจน์แนวคิดนี้ จะนำมาใช้ได้หรือไม่

ซึ่งหลักการของ Canvas คือการกำหนดพื้นที่ ความกว้าง ความสูง สำหรับวาด (ในตัวอย่างคือ 300x150)

โดยลากเส้นผ่านจุด x, y ดังนี้

  • moveTo(20, 110) เป็นจุดเริ่มต้น x=20, y=110
  • lineTo(110, 20) ลากเส้นไปที่ตำแหน่ง x=110, y=20

ทดลองวาดเส้นแบบง่าย ๆ

Live Editor
() => {
    const canvas = useRef()
    
    const draw = () => {
        const ctx = canvas.current.getContext("2d")
        ctx.beginPath()
        ctx.lineWidth = 2 // ขนาดของเส้น
        ctx.strokeStyle = "red" // สีของเส้น
        ctx.moveTo(20, 110) // กำหนดจุดเริ่มต้น
        ctx.lineTo(110, 20) // ลากเส้นจากจุดเริ่มต้นไปที่จุดที่ต้องการ
        ctx.stroke() // วาดเส้น
        ctx.closePath()
    }
    
    useEffect(() => {
        draw()
    }, [])
    
    return <>
        <canvas ref={canvas} width="300" height="150" />
    </>
}
Result
Loading...

ทดลองนำรูปภาพมาทำเป็น Canvas

  • เมื่อเราเข้าใจหลักการ Canvas และสามารถวาดเส้นบนพิกัด x,y ได้แล้ว
  • มาทดลองนำรูปภาพมาทำเป็น Canvas (เพื่อจำลองว่า หากเป็นรูปภาพจริงจะมีลักษณะอย่างไร)
  • โดยสิ่งที่เพิ่มขึ้นมาคือ new Image() ซึ่งเป็นการสร้าง Object ของรูปภาพขึ้นมา
  • จากนั้นกำหนดขนาด แล้วใช้คำสั่ง drawImage() เพื่อวาดรูปภาพเป็น Canvas
  • ทดสอบวาดเส้นตามพิกัดที่ลองกำหนดไว้
Live Editor
() => {
    const canvas = useRef()
    const chartSize = { width: 610, height: 367 }
    
    const draw = () => {
        const ctx = canvas.current.getContext("2d")
        
        const image = new Image() // สร้าง Object ของรูปภาพขึ้นมา
        image.src = 'https://i.imgur.com/kvDsyOr.jpg' // กำหนดที่อยู่ของรูปภาพ
        image.onload = () => {
            if (ctx) {
                const size = [0, 0, ...Object.values(chartSize)] // กำหนดขนาดของรูปภาพ
                ctx.drawImage(image, ...size) // เขียนรูปภาพลง Canvas

                ctx.beginPath()
                ctx.lineWidth = 2
                ctx.strokeStyle = "red"
                ctx.moveTo(243, 259)
                ctx.lineTo(346, 118)
                ctx.lineTo(413, 199)
                ctx.lineTo(549, 97)
                ctx.stroke()
                ctx.closePath()
            }
        }
    }
    
    useEffect(() => {
        draw()
    }, [])
    
    return <>
        <canvas ref={canvas} {...chartSize} />
    </>
}
Result
Loading...

เริ่ม Implements

ขั้นตอนที่ 1

  • ตามหลักการที่ได้ทดลองคือการลากเส้นไปที่จุด x,y
  • นั่นก็หมายความว่าเราต้องรู้ทุกจุด x,y ของรูปภาพ
  • นั่นเป็นงานที่เยอะมากเลยทีเดียว จะแก้ปัญหานี้อย่างไร

แก้ปัญหาการหาพิกัดด้วยสูตรคำนวณ

  • ขนาดระยะห่างจากจุดเริ่มต้น 2 แกน (แกน x แนวนอน) (แกน y แนวตั้ง) คือ offset
  • ให้ x โดยที่ 2 = 1, 6 = 2, 10 = 3, 14 = 4, 18 = 5, 22 = 6
  • ให้ y โดยที่ Modulus ด้วย 35 (เพื่อเอาเศษ)
  • ดังนั้นเมื่อ ชั่วโมงที่ 2, Temp 36 ก็จะได้ x = offset * (1) = 10, y = offset * (35 % 35) = 10
  • ดังนั้นเมื่อ ชั่วโมงที่ 14, Temp 38 ก็จะได้ x = offset * (4) = 40, y = offset * (35 % 38) = 30
  • ดังนั้นเมื่อ ชั่วโมงที่ 18, Temp 40.5 ก็จะได้ x = offset * (5) = 50, y = offset * (35 % 40.5) = 55

การแทรกตัวเลขและข้อความลงใน Canvas

หลักการแทรกตัวเลขและข้อความลงใน Canvas

  • ctx.font = 'Italic 14px Arial'; // ขนาดข้อความ และรูปแบบ font
  • ctx.fillStyle = 'black'; // สีของข้อความ
  • ctx.textAlign = 'center'; // กำหนดการจัดตำแหน่ง (ถ้าต้องการ)
  • ctx.fillText(label, x, x); // คำสั่งแทรกข้อความ
Live Editor
() => {
    const canvas = useRef()
    const chartSize = { width: 610, height: 367 }
    
    const draw = () => {
        const ctx = canvas.current.getContext("2d");
        const image = new Image()
        image.src = 'https://i.imgur.com/kvDsyOr.jpg'
        image.onload = () => {
            if (ctx) {
                const size = [0, 0, ...Object.values(chartSize)]
                ctx.drawImage(image, ...size)

                // ตัวอย่างการแทรกตัวเลข
                ctx.font = 'bold 14px Arial'; // ขนาดข้อความ และรูปแบบ font
                ctx.fillStyle = 'black'; // สีของข้อความ
                ctx.textAlign = 'center'; // กำหนดการจัดตำแหน่ง (ถ้าต้องการ)
                ctx.fillText('123', 243, 263); // คำสั่งแทรกข้อความ
                ctx.fillText('456', 346, 122); // คำสั่งแทรกข้อความ
                ctx.fillText('789', 413, 202); // คำสั่งแทรกข้อความ
                ctx.fillText('111', 549, 101); // คำสั่งแทรกข้อความ

                // ตัวอย่างการแทรกข้อความ
                ctx.textAlign = 'left'; // กำหนดการจัดตำแหน่ง
                ctx.fillText('List of items', 50, 60); // คำสั่งแทรกข้อความ
                ctx.font = '12px Arial';
                ctx.fillText('item 1', 50, 82); // คำสั่งแทรกข้อความ
                ctx.fillText('item 2', 50, 102); // คำสั่งแทรกข้อความ
                ctx.fillText('item 3', 50, 122); // คำสั่งแทรกข้อความ
            }
        }
    }
    
    useEffect(() => {
        draw()
    }, [])
    
    return <>
        <canvas ref={canvas} {...chartSize} />
    </>
}
Result
Loading...

ขั้นตอนที่ 2

  • สร้างรูปภาพสำหรับใช้กับ Canvas บน Excel (โครงรูปว่างเปล่าที่ไม่มีข้อมูล)
  • แบ่งรูปภาพเป็น 2 ส่วน เพื่อให้ง่ายต่อการ Render และนำข้อมูลมาเติมใส่ลงรูปภาพ
  • Grouping ช่วงข้อมูลให้อยู่ในกลุ่มของชั่วโมง 2 - 22

เมื่อ Implement เสร็จ

Vital Signs Chart

สรุป

ความยุ่งยากของงานนี้คือ

  1. เป็นรูปแบบงานที่ไม่เคยทำมาก่อน
  2. ต้องหาข้อมูลและวิธีการเพื่อทำให้ได้ตามที่ผู้ใช้บอกว่า "จะเอาแบบนี้"
  3. พิกัด x, y สามารถคำนวณได้จากแนวคิดดังนี้
    • กำหนดจุดตั้งต้น
    • หาขนาดระยะห่างจากจุดเริ่มต้น 2 แกน (แกน x แนวนอน) (แกน y แนวตั้ง)
    • เมื่อต้องการวาดเส้นตามพิกัดก็ใช้ ขนาดแกน x จำนวนช่องที่ต้องการลากเส้น
  4. ถ้ารูปภาพมีการแก้ไข หรือเปลี่ยนขนาดใหม่ พิกัด อาจจะคลาดเคลื่อนได้
  5. รองรับการดูทั้งบนมือถือ และ PC ในขนาดหน้าจอที่แตกต่างกัน
  6. ที่จริงการ Group ข้อมูลให้อยู่ในช่วงเวลาชั่วโมงของวันก็มีความยุ่งยาก ก็ใช้เทคนิคหลายแบบที่ไม่ใช่แค่ทำงานได้ แต่ต้องทำงานได้มีประสิทธิภาพ และรวดเร็ว

References

Loading...