เมื่อต้องสร้างกราฟ Vital Signs ฉบับตามใจผู้ใช้ "จะเอาแบบนี้" แต่เหนื่อยคนทำ (ระบบ EMR)
Table of contents
Intro
สวัสดีครับ บทความนี้น่าจะเป็นบทความแรกที่เล่าเกี่ยวกับงานที่ทำอยู่ที่ทำงาน ซึ่งเป็นช่วงที่ได้รับ Assigned ให้ทำระบบ EMR
EMR ถ้าจะให้อธิบายง่าย ๆ สั้น ๆ คือระบบที่เป็นเหมือนแฟ้มประวัติการรักษาคนไข้ ซึ่งภายในก็จะประกอบได้วยหลายส่วน (Sections) เช่น ผล Labs, Xray, Med Orders, Vital Signs Chart และอื่น ๆ ขอไม่ลงรายละเอียดลึก
โดยวันนี้จะมาพูดถึงแค่ Vital Signs Chart ซึ่งก็เป็นส่วนหนึ่งของ EMR และอันที่จริงก็จะไม่ลงรายละเอียดว่าคืออะไร เพราะจุดประสงค์ของบทความนี้ต้องการจะเล่าถึงวิธีทำ เพื่อให้ได้ตามที่ผู้ใช้งานต้องการ 😄
จะเอาแบบนี้
เมื่อคนสั่งงานส่งรูปภาพนี้มาให้ดู เป็นงานที่ต้องหยุดคิดเลยว่าจะทำยังไงดี เพราะผู้ใช้งานบอกว่า "อยากได้แบบนี้" อาจจะเป็นเพราะดูข้อมูลง่าย มองข้อมูลที่มีความสัมพันธ์กันในคอลั่มน์ตามวันและเวลา
แต่ในใจ ปัญหาแรกที่คิดคือ "จะทำยังไงดี ไม่เคยทำงานกราฟแนวนี้เลย" 😅
รูปภาพนี้ ไม่แน่ใจแหล่งที่มา น่าจะมาจาก HIS สักที่ที่ใช้ภายใน รพ.
วิเคราะห์และความท้าทาย
- ช่องช่วงเวลาชั่วโมง (คอลั่มน์) ของข้อมูลนั้น ๆ (ทั้งด้านบน ด้านล่าง) ต้องสัมพันธ์กัน
- มีทั้งการวาดเส้นของกราฟ และตารางข้อมูล
- ให้รองรับการดูทั้งบน PC (Desktop) และ มือถือ (Mobile) โดยที่ไม่ต้องย่อหรือลดขนาดกราฟแต่ให้สามารถ Scroll ซ้าย-ขวา ได้
ค้นคว้าหาข้อมูล
หลังจากที่ได้รับมอบหมายงาน อย่างแรกเลยเริ่มหาตัวช่วยก่อน
- ไปดูว่า HighCharts demos ว่ามีตัวอย่างไหนคล้าย ๆ แนวที่ต้องการบ้าง
- ถ้าจะสร้างตารางข้อมูลขึ้นมา แล้วเอารูปภาพของกราฟมาเป็นพื้นหลัง
สรุปได้ว่าทั้งตัวอย่างและแนวคิดที่ลองหาดูบน 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
ทดลองวาดเส้นแบบง่าย ๆ
() => { 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" /> </> }
ทดลองนำรูปภาพมาทำเป็น Canvas
- เมื่อเราเข้าใจหลักการ Canvas และสามารถวาดเส้นบนพิกัด x,y ได้แล้ว
- มาทดลองนำรูปภาพมาทำเป็น Canvas (เพื่อจำลองว่า หากเป็นรูปภาพจริงจะมีลักษณะอย่างไร)
- โดยสิ่งที่เพิ่มขึ้นมาคือ new Image() ซึ่งเป็นการสร้าง Object ของรูปภาพขึ้นมา
- จากนั้นกำหนดขนาด แล้วใช้คำสั่ง drawImage() เพื่อวาดรูปภาพเป็น Canvas
- ทดสอบวาดเส้นตามพิกัดที่ลองกำหนดไว้
() => { 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} /> </> }
เริ่ม 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); // คำสั่งแทรกข้อความ
() => { 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} /> </> }
ขั้นตอนที่ 2
- สร้างรูปภาพสำหรับใช้กับ Canvas บน Excel (โครงรูปว่างเปล่าที่ไม่มีข้อมูล)
- แบ่งรูปภาพเป็น 2 ส่วน เพื่อให้ง่ายต่อการ Render และนำข้อมูลมาเติมใส่ลงรูปภาพ
- Grouping ช่วงข้อมูลให้อยู่ในกลุ่มของชั่วโมง 2 - 22
- ใช้ค่าตัวเลขในการแทรกข้อความลงใน Canvas
เมื่อ Implement เสร็จ
สรุป
ความยุ่งยากของงานนี้คือ
- เป็นรูปแบบงานที่ไม่เคยทำมาก่อน
- ต้องหาข้อมูลและวิธีการเพื่อทำให้ได้ตามที่ผู้ใช้บอกว่า "จะเอาแบบนี้"
- พิกัด x, y สามารถคำนวณได้จากแนวคิดดังนี้
- กำหนดจุดตั้งต้น
- หาขนาดระยะห่างจากจุดเริ่มต้น 2 แกน (แกน x แนวนอน) (แกน y แนวตั้ง)
- เมื่อต้องการวาดเส้นตามพิกัดก็ใช้ ขนาดแกน x จำนวนช่องที่ต้องการลากเส้น
- ถ้ารูปภาพมีการแก้ไข หรือเปลี่ยนขนาดใหม่ พิกัด อาจจะคลาดเคลื่อนได้
- รองรับการดูทั้งบนมือถือ และ PC ในขนาดหน้าจอที่แตกต่างกัน
- ที่จริงการ Group ข้อมูลให้อยู่ในช่วงเวลาชั่วโมงของวันก็มีความยุ่งยาก ก็ใช้เทคนิคหลายแบบที่ไม่ใช่แค่ทำงานได้ แต่ต้องทำงานได้มีประสิทธิภาพ และรวดเร็ว