import { Component, ComponentNature, POSITION, RectPath, Shape, sceneComponent } from '@hatiolab/things-scene' import { safeRound } from './utils/safe-round' import { ParentObjectMixin, ParentObjectMixinProperties } from './features/parent-object-mixin' /** * 캐리어 ID 정상/비정상 비율 게이지 (Normal / Abnormal) * * 스토커에 보관된 캐리어들의 정상/비정상 비율을 세그먼트 게이지로 표시한다. * Portrait(세로) 모드에서는 아래→위, Landscape(가로) 모드에서는 왼→오 방향으로 채운다. */ interface Segment { label: string count: number ratio: number color: string } const COLOR_NORMAL = '#8BDA5B' const COLOR_ABNORMAL = '#F4B4B4' const NATURE: ComponentNature = { mutable: false, resizable: true, rotatable: true, properties: [ ...ParentObjectMixinProperties, { type: 'number', name: 'carrierNormal', label: 'carrier-normal' }, { type: 'number', name: 'carrierAbnormal', label: 'carrier-abnormal' }, { type: 'number', label: 'round', name: 'round', property: { min: 0 } } ] } var controlHandler = { ondragmove: function (point: POSITION, index: number, component: Component) { var { left, top, width, height } = component.model var transcoorded = component.transcoordP2S(point.x, point.y) var round = ((transcoorded.x - left) / (width / 2)) * 100 round = safeRound(round, width, height) component.set({ round }) } } @sceneComponent('StockerAbnormalBar') export default class StockerAbnormalBar extends ParentObjectMixin(RectPath(Shape)) { static get nature() { return NATURE } get controls() { var { left, top, width, round, height } = this.state round = round == undefined ? 0 : safeRound(round, width, height) return [ { x: left + (width / 2) * (round / 100), y: top, handler: controlHandler } ] } isIdentifiable() { return false } get segments(): Segment[] { const { carrierNormal = 0, carrierAbnormal = 0 } = this.state const total = carrierNormal + carrierAbnormal if (total === 0) { return [ { label: 'Normal', count: 0, ratio: 0, color: COLOR_NORMAL }, { label: 'Abnormal', count: 0, ratio: 0, color: COLOR_ABNORMAL } ] } return [ { label: 'Normal', count: carrierNormal, ratio: carrierNormal / total, color: COLOR_NORMAL }, { label: 'Abnormal', count: carrierAbnormal, ratio: carrierAbnormal / total, color: COLOR_ABNORMAL } ] } render(context: CanvasRenderingContext2D) { const { width, height } = this.bounds if (width >= height) { this.renderLandscape(context) } else { this.renderPortrait(context) } } renderPortrait(context: CanvasRenderingContext2D) { const { left, top, width, height } = this.bounds const { lineWidth, strokeStyle, round, fillStyle = 'white' } = this.state const segments = this.segments context.save() context.translate(left, top) // background context.beginPath() context.fillStyle = fillStyle context.roundRect(0, 0, width, height, round) context.fill() context.clip() // segments (아래→위: Abnormal이 아래, Normal이 위) let y = height for (const seg of [...segments].reverse()) { const segHeight = height * seg.ratio if (segHeight <= 0) continue y -= segHeight context.beginPath() context.fillStyle = seg.color context.rect(0, y, width, segHeight) context.fill() this._drawSegmentLabel(context, seg, 0, y, width, segHeight) } // outline context.beginPath() context.setLineDash([]) context.strokeStyle = strokeStyle context.lineWidth = lineWidth context.roundRect(0, 0, width, height, round) context.stroke() context.translate(-left, -top) context.restore() } renderLandscape(context: CanvasRenderingContext2D) { const { left, top, width, height } = this.bounds const { lineWidth, strokeStyle, round, fillStyle = 'white' } = this.state const segments = this.segments context.save() context.translate(left, top) // background context.beginPath() context.fillStyle = fillStyle context.roundRect(0, 0, width, height, round) context.fill() context.clip() // segments (왼→오) let x = 0 for (const seg of segments) { const segWidth = width * seg.ratio if (segWidth <= 0) continue context.beginPath() context.fillStyle = seg.color context.rect(x, 0, segWidth, height) context.fill() this._drawSegmentLabel(context, seg, x, 0, segWidth, height) x += segWidth } // outline context.beginPath() context.setLineDash([]) context.strokeStyle = strokeStyle context.lineWidth = lineWidth context.roundRect(0, 0, width, height, round) context.stroke() context.translate(-left, -top) context.restore() } private _drawSegmentLabel( context: CanvasRenderingContext2D, seg: Segment, x: number, y: number, w: number, h: number ) { const percent = Math.round(seg.ratio * 100) const lines = [seg.label, `${percent}%`] const fontSize = Math.max(9, Math.min(14, Math.min(w, h) * 0.22)) context.font = `bold ${fontSize}px sans-serif` context.fillStyle = '#333' context.textAlign = 'center' context.textBaseline = 'middle' const lineHeight = fontSize * 1.2 const totalHeight = lines.length * lineHeight const startY = y + (h - totalHeight) / 2 + lineHeight / 2 for (let i = 0; i < lines.length; i++) { context.fillText(lines[i], x + w / 2, startY + i * lineHeight) } } onchangeData(after: Record, before: Record): void { const { CARRIER_NORMAL = 0, CARRIER_ABNORMAL = 0 } = this.data && typeof this.data == 'object' ? this.data : {} this.setState({ carrierNormal: Number(CARRIER_NORMAL) || 0, carrierAbnormal: Number(CARRIER_ABNORMAL) || 0 }) } }