Skip to content

Instantly share code, notes, and snippets.

@kimji1
Last active February 23, 2025 14:23
Show Gist options
  • Save kimji1/7117ac9037890a62311c7fb5e1761dd4 to your computer and use it in GitHub Desktop.
Save kimji1/7117ac9037890a62311c7fb5e1761dd4 to your computer and use it in GitHub Desktop.
@Composable
fun PointLineChart(
modifier: Modifier = Modifier,
state: PointLineChartState,
focusUpdateState: OuterFocusUpdateState = rememberOuterFocusUpdateState(),
) {
val textMeasurer = rememberTextMeasurer()
val mutablePoints: SnapshotStateList<LineChartPoint> = remember(state.points) {
state.points.toMutableStateList()
}
val topDataAreaHeight = state.topDataAreaHeight.toPx()
val chartHeight = state.topDataAreaHeight + state.contentAreaHeight
var focusedPoint by remember(state.points) { mutableStateOf<LineChartPoint?>(null) }
val isOffsetCalculated by remember(state.points) { derivedStateOf { mutablePoints.all { it.isOffsetCalculated } } }
fun changeFocused(item: LineChartPoint?, shouldRequestUpdate: Boolean = true) {
focusedPoint = if (item != null && item.focused.not()) {
val index = mutablePoints.indexOf(item)
mutablePoints.replaceAll { it.copy(focused = false) }
mutablePoints[index] = mutablePoints[index].copy(focused = true)
mutablePoints[index]
} else {
mutablePoints.replaceAll { it.copy(focused = false) }
null
}
if (shouldRequestUpdate) {
focusUpdateState.requestUpdateFocusedPoint(focusedPoint)
}
}
LaunchedEffect(isOffsetCalculated, focusUpdateState.shouldUpdateFocusedPoint) {
if (focusUpdateState.shouldUpdateFocusedPoint.not()) {
return@LaunchedEffect
}
val outerFocusedPoint = mutablePoints.filter { it.value != null }
.firstOrNull { focusUpdateState.isFocusedPoint(it) }
changeFocused(item = outerFocusedPoint, shouldRequestUpdate = false)
focusUpdateState.onUpdatedFocusedPoint()
}
val defaultColor = state.pointConfig.defaultColor
val pointColor = state.pointConfig.pointColor
var chartSize by remember { mutableStateOf<Size?>(null) }
val brush by remember(chartSize, state.pointConfig) {
val defaultBrush = mutableStateOf<Brush?>(SolidColor(defaultColor))
val config = state.chartBackgroundConfig ?: return@remember defaultBrush
val highlightingAreaTopYPos = config.highlightingAreaYPos - topDataAreaHeight
val highlightingAreaBottomYPos = highlightingAreaTopYPos + config.highlightingAreaHeight
mutableStateOf<Brush?>(
chartSize?.let {
createShaderBrush(
size = it,
areas = listOf(
Area(0f, highlightingAreaTopYPos, defaultColor),
Area(highlightingAreaTopYPos, highlightingAreaBottomYPos, pointColor),
Area(highlightingAreaBottomYPos, it.height, defaultColor)
)
)
}
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentSize()
.then(modifier)
) {
Column(modifier = Modifier.wrapContentSize()) {
state.title?.let {
ChartTitleText(
text = it,
modifier = Modifier.padding(bottom = 24.dp)
)
}
Box(modifier = Modifier
.height(chartHeight)
.onSizeChanged { chartSize = it.toSize() }) {
if (state.chartBackgroundConfig != null) {
ChartBackground(
chartHeight = chartHeight.toPx(),
config = state.chartBackgroundConfig
)
}
LineChart(
textMeasurer = textMeasurer,
pointConfig = state.pointConfig,
mutablePoints = mutablePoints,
contentAreaHeight = state.contentAreaHeight,
topDataAreaHeight = state.topDataAreaHeight,
defaultColor = defaultColor,
pointColor = pointColor,
brush = brush,
changeFocused = ::changeFocused,
)
Row(modifier = Modifier.matchParentSize()) {
repeat(state.points.size) {
Canvas(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
) {
drawVerticalDash(
lineColor = state.dividerConfig.color,
xOffset = 0f,
yTopOffset = 0f,
yBottomOffset = size.height,
strokeWidth = state.dividerConfig.width
)
}
}
}
focusedPoint?.let {
PointDetailPopup(
textMeasurer = textMeasurer,
parentSize = chartSize,
point = it,
pointConfig = state.pointConfig,
changeFocused = ::changeFocused
)
}
}
HorizontalDivider(
thickness = 1.dp,
color = VCTheme.colors.secondaryLine
)
NameTag(
items = state.points.map { it.name to if (it.isColoredName) VCTheme.colors.error else VCTheme.colors.onSurface0 },
groupingUnit = state.groupingUnit,
dividerConfig = state.dividerConfig
)
if (state.footNoteData != null) {
FootNote(footNoteData = state.footNoteData)
}
}
}
}
@NonRestartableComposable
@Composable
fun ChartTitleText(
text: String,
modifier: Modifier = Modifier
) = Text(
text = text,
style = VCTheme.typo.bodyB,
color = VCTheme.colors.onSurface0,
modifier = modifier
)
@Composable
fun PointDetailPopup(
textMeasurer: TextMeasurer,
parentSize: Size?,
point: LineChartPoint,
pointConfig: PointConfig,
changeFocused: (LineChartPoint) -> Unit
) {
parentSize ?: return
val strokeWidth = 1.dp.toPx()
val color = VCTheme.colors.onSurface0
val valueTextStyle = VCTheme.typo.bodyB
val nameTextStyle = VCTheme.typo.caption1B
val valueWidth = textMeasurer.measure(point.valueDetailDisplay, valueTextStyle).size.width
val secondaryValueWidth =
point.secondaryDetailDisplay?.let { textMeasurer.measure(it, valueTextStyle).size.width }
?: 0
val nameWidth = textMeasurer.measure(point.detailName, nameTextStyle).size.width
val halfWidth = maxOf(valueWidth, secondaryValueWidth, nameWidth) / 2
val circleRadius = pointConfig.focusedPointSize.toPx() / 2
val (xOffset, textAlign) = (point.offset.x - halfWidth - circleRadius).let { xOffset ->
if (xOffset < 0f) {
0f to Alignment.Start
} else if (xOffset + (halfWidth * 2) > parentSize.width) {
(parentSize.width - halfWidth * 2f) to Alignment.End
} else xOffset to Alignment.CenterHorizontally
}
Canvas(
modifier = Modifier
.width(pointConfig.focusedPointSize)
.fillMaxHeight()
.offset(x = point.offset.x.toDp())
.noRippleClickable { changeFocused(point) },
) {
drawLine(
color = color,
start = Offset.Zero,
end = Offset(x = 0f, y = size.height),
strokeWidth = strokeWidth
)
drawCircle(
color = color,
radius = pointConfig.focusedPointSize.toPx() / 2,
center = point.offset.copy(x = 0f)
)
drawCircle(
color = pointConfig.backgroundColor,
radius = pointConfig.strokeWidth.toPx(),
center = point.offset.copy(x = 0f)
)
}
Column(
modifier = Modifier
.wrapContentSize()
.padding(horizontal = 4.dp)
.offset(x = xOffset.toDp())
.background(color = pointConfig.backgroundColor)
.noRippleClickable { changeFocused(point) },
horizontalAlignment = textAlign
) {
Text(
text = point.valueDetailDisplay,
style = valueTextStyle.copy(color = point.focusedColor),
)
point.secondaryDetailDisplay?.let {
Text(
text = it,
style = valueTextStyle.copy(color = color)
)
}
Text(
text = point.detailName,
style = nameTextStyle.copy(color = color)
)
}
}
@Composable
private fun BoxScope.LineChart(
textMeasurer: TextMeasurer,
mutablePoints: MutableList<LineChartPoint>,
pointConfig: PointConfig,
contentAreaHeight: Dp,
topDataAreaHeight: Dp,
defaultColor: Color,
pointColor: Color,
brush: Brush?,
changeFocused: (LineChartPoint) -> Unit,
) {
val baseLine = (pointConfig.valueRange.max - pointConfig.valueRange.min)
val unit = contentAreaHeight.toPx() / baseLine
val dotRadius = pointConfig.dotSize.toPx() / 2
fun calculateYOffset(value: Float): Float {
return ((pointConfig.valueRange.max - value) * unit)
}
ChartLine(
contentAreaHeight = contentAreaHeight,
mutablePoints = mutablePoints,
calculateYOffset = ::calculateYOffset,
pointConfig = pointConfig,
brush = brush
)
ChartPoint(
topDataAreaHeight = topDataAreaHeight,
contentAreaHeight = contentAreaHeight,
mutablePoints = mutablePoints,
dotRadius = dotRadius,
pointConfig = pointConfig,
pointColor = pointColor,
defaultColor = defaultColor,
calculateYOffset = ::calculateYOffset,
changeFocused = changeFocused,
)
ChartPointValue(
textMeasurer = textMeasurer,
contentAreaHeight = contentAreaHeight,
topDataAreaHeight = topDataAreaHeight,
mutablePoints = mutablePoints,
pointConfig = pointConfig,
pointColor = pointColor,
defaultColor = defaultColor,
calculateYOffset = ::calculateYOffset
)
}
@Composable
fun BoxScope.ChartLine(
contentAreaHeight: Dp,
mutablePoints: MutableList<LineChartPoint>,
calculateYOffset: (Float) -> Float,
pointConfig: PointConfig,
brush: Brush?
) {
val pathValues by remember(mutablePoints) {
mutableStateOf(mutablePoints
.mapIndexed { index, lineChartPoint ->
lineChartPoint.value?.let {
val value = it.roundRange(pointConfig.valueRange)
index to value
}
}
.filterNotNull()
)
}
val bottom = contentAreaHeight.toPx() - pointConfig.dotSize.value * 1.5f
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(contentAreaHeight)
.align(Alignment.BottomCenter)
) {
val width = size.width / mutablePoints.size
fun calculateXOffset(index: Int) = (width * index) + width / 2
val path = Path()
pathValues.forEachIndexed { index, value ->
val point = Offset(
x = calculateXOffset(index + (value.first - index)),
y = min(bottom, calculateYOffset(value.second))
)
if (index == 0) {
path.moveTo(point.x, point.y)
}
val nextPoint = if (index == pathValues.lastIndex) null else {
val nextIndex = index + 1
val nextValue = pathValues.subList(nextIndex, pathValues.size)
.firstOrNull() ?: return@forEachIndexed
Offset(
x = calculateXOffset(nextIndex + nextValue.first - nextIndex),
y = min(bottom, calculateYOffset(nextValue.second))
)
} ?: return@forEachIndexed
path.cubicTo(
point.x,
point.y,
nextPoint.x,
nextPoint.y,
nextPoint.x,
nextPoint.y
)
}
if (brush != null) {
drawPath(
path = path,
brush = brush,
style = Stroke(width = pointConfig.strokeWidth.toPx())
)
}
}
}
@Composable
fun BoxScope.ChartPoint(
topDataAreaHeight: Dp,
contentAreaHeight: Dp,
mutablePoints: MutableList<LineChartPoint>,
dotRadius: Float,
pointConfig: PointConfig,
pointColor: Color,
defaultColor: Color,
calculateYOffset: (Float) -> Float,
changeFocused: (LineChartPoint) -> Unit,
) {
val bottom = calculateYOffset(pointConfig.valueRange.min) - dotRadius
fun updatePointOffset(index: Int, offset: Offset, color: Color) {
mutablePoints[index] = (mutablePoints[index].copy(
offset = offset,
focusedColor = color,
isOffsetCalculated = true
))
}
Row(
modifier = Modifier
.height(contentAreaHeight)
.align(Alignment.BottomCenter)
) {
mutablePoints.forEachIndexed { index, item ->
if (item.value == null) {
Spacer(modifier = Modifier.weight(1f))
return@forEachIndexed
}
var positionInParent by remember { mutableStateOf(Offset.Zero) }
var halfWidth by remember { mutableFloatStateOf(0f) }
val hasGuideline = pointConfig.guideline != null
val shouldPointing = hasGuideline && pointConfig.guideline?.let {
pointConfig.pointComparisonRule.check(item.value, it.max) &&
ComparisonRule.More.check(item.value, it.min)
} ?: false
val itemColor = if (shouldPointing) pointColor else defaultColor
val value = item.value.roundRange(pointConfig.valueRange)
val yPos = min(bottom, calculateYOffset(value))
LaunchedEffect(positionInParent, halfWidth) {
if (positionInParent == Offset.Zero && halfWidth == 0f) {
return@LaunchedEffect
}
val itemOffset = Offset(x = halfWidth, y = yPos + topDataAreaHeight.toPx())
updatePointOffset(
index = index,
offset = itemOffset + positionInParent,
color = itemColor
)
}
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.onGloballyPositioned {
positionInParent = Offset(x = it.positionInParent().x, y = 0f)
}
) {
Canvas(modifier = Modifier
.matchParentSize()
.conditional(item.valueDetailDisplay.isNotEmpty()) {
noRippleClickable {
changeFocused(mutablePoints[index])
}
}
) {
halfWidth = size.width / 2
drawCircle(
color = itemColor,
radius = dotRadius,
center = Offset(halfWidth, yPos)
)
drawCircle(
color = pointConfig.backgroundColor,
radius = pointConfig.strokeWidth.toPx(),
center = Offset(halfWidth, yPos)
)
}
}
}
}
}
@Composable
fun BoxScope.ChartPointValue(
textMeasurer: TextMeasurer,
contentAreaHeight: Dp,
topDataAreaHeight: Dp,
mutablePoints: MutableList<LineChartPoint>,
pointConfig: PointConfig,
pointColor: Color,
defaultColor: Color,
calculateYOffset: (Float) -> Float
) {
val dotRadius = pointConfig.dotSize.value / 2
val bottom = calculateYOffset(0f) - dotRadius
Row(
modifier = Modifier
.height(contentAreaHeight)
.align(Alignment.BottomCenter)
) {
mutablePoints.forEach { item ->
if (item.value == null) {
Spacer(modifier = Modifier.weight(1f))
return@forEach
}
val yPos = min(bottom, calculateYOffset(item.value.roundRange(pointConfig.valueRange)))
val hasGuideline = pointConfig.guideline != null
val shouldPointing = hasGuideline && pointConfig.guideline?.let {
pointConfig.pointComparisonRule.check(item.value, it.max) &&
ComparisonRule.More.check(item.value, it.min)
} ?: false
val itemColor = if (shouldPointing) pointColor else defaultColor
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.wrapContentWidth(unbounded = true)
) {
if (pointConfig.hideValue.not() && item.focused.not() && item.valueDisplay.isNotBlank()) {
OutlineText(
textMeasurer = textMeasurer,
text = item.valueDisplay,
textColor = itemColor,
textStyle = VCTheme.typo.caption1B,
topDataAreaHeight = topDataAreaHeight,
yPos = yPos,
pointConfig = pointConfig
)
}
}
}
}
}
@Composable
private fun BoxScope.OutlineText(
textMeasurer: TextMeasurer,
text: String,
textColor: Color,
textStyle: TextStyle,
topDataAreaHeight: Dp,
yPos: Float,
pointConfig: PointConfig
) {
val textMeasuredSize = textMeasurer.measure(
text = text,
style = textStyle
).size
val yOffset = ((yPos - textMeasuredSize.height - 12.dp.toPx()).toDp())
.let { yOffset -> if (yOffset < -topDataAreaHeight) -topDataAreaHeight else yOffset }
Text(
modifier = Modifier
.offset(x = 0.dp, y = yOffset)
.align(Alignment.TopCenter),
text = text,
style = textStyle.copy(
color = pointConfig.backgroundColor,
drawStyle = Stroke(
width = 4.dp.toPx(),
join = StrokeJoin.Round
)
)
)
Text(
modifier = Modifier
.offset(x = 0.dp, y = yOffset)
.align(Alignment.TopCenter),
text = text,
style = textStyle.copy(color = textColor)
)
}
private fun Float.roundRange(range: Range) = if (this < range.min) {
range.min
} else if (this > range.max) {
range.max
} else this
data class PointLineChartState(
val title: String?,
val points: List<LineChartPoint>,
val pointConfig: PointConfig,
val topDataAreaHeight: Dp,
val contentAreaHeight: Dp,
val groupingUnit: Int,
val dividerConfig: DividerConfig,
val chartBackgroundConfig: ChartBackgroundConfig?,
val footNoteData: FootNoteData?
)
@Composable
fun rememberLineChartState(
points: List<LineChartPoint>,
groupingUnit: Int,
pointConfig: PointConfig,
useBackgroundHighlighting: Boolean = true,
title: String? = null,
topDataAreaHeight: Dp = 28.dp,
contentAreaHeight: Dp = 200.dp,
dividerConfig: DividerConfig = DividerConfig(
color = VCTheme.colors.secondaryLine,
width = 1.dp.toPx()
),
footNoteData: FootNoteData? = null,
): PointLineChartState {
fun calculateBackgroundConfig(): Pair<Float, Float> {
val guidelineRangeGap = if (pointConfig.guideline != null) {
pointConfig.guideline.max - pointConfig.guideline.min
} else 0f
val valueRangeGap = (pointConfig.valueRange.max - pointConfig.valueRange.min)
val highlightingAreaRatio = guidelineRangeGap / valueRangeGap
val highlightingAreaHeight = contentAreaHeight.toPx() * highlightingAreaRatio
val unit = contentAreaHeight.toPx() / valueRangeGap
val highlightingAreaYPos = if (pointConfig.guideline != null) {
topDataAreaHeight.toPx() + (unit * (pointConfig.valueRange.max - pointConfig.guideline.max))
} else 0f
return highlightingAreaHeight to highlightingAreaYPos
}
val chartBackgroundConfig = if (useBackgroundHighlighting) {
val (highlightingAreaHeight, highlightingAreaYPos) = calculateBackgroundConfig()
createChartBackgroundConfig(
highlightingAreaHeight = highlightingAreaHeight,
highlightingAreaYPos = highlightingAreaYPos,
)
} else null
return remember(points) {
PointLineChartState(
title = title,
points = points,
pointConfig = pointConfig,
groupingUnit = groupingUnit,
topDataAreaHeight = topDataAreaHeight,
contentAreaHeight = contentAreaHeight,
dividerConfig = dividerConfig,
chartBackgroundConfig = chartBackgroundConfig,
footNoteData = footNoteData
)
}
}
enum class ComparisonRule {
Below, // 이하
Under, // 미만
More, // 초과
Over; // 초과
fun check(target: Float, base: Float) = when (this) {
Below -> target <= base
Under -> target < base
More -> target >= base
Over -> target > base
}
}
data class LineChartPoint(
val value: Float?,
val valueDisplay: String,
val valueDetailDisplay: String = "",
val secondaryDetailDisplay: String? = null,
val name: String,
val detailName: String,
val isColoredName: Boolean = false,
val focused: Boolean = false,
val isOffsetCalculated: Boolean = false,
val offset: Offset = Offset.Zero,
val focusedColor: Color = Color.Black
)
data class PointConfig(
val guideline: Range?,
val valueRange: Range,
val dotSize: Dp,
val focusedPointSize: Dp,
val pointComparisonRule: ComparisonRule,
val strokeWidth: Dp,
val defaultColor: Color,
val pointColor: Color,
val backgroundColor: Color,
val hideValue: Boolean
)
data class Range(val min: Float, val max: Float)
@Composable
fun createPointConfig(
valueRange: Range,
guideline: Range? = null,
hideValue: Boolean = false,
pointComparisonRule: ComparisonRule = ComparisonRule.Below,
pointSize: Dp = 8.dp,
focusedPointSize: Dp = 10.dp,
strokeWidth: Dp = 2.dp,
defaultColor: Color = VCTheme.colors.error,
pointColor: Color = VCTheme.colors.graphSafety40,
backgroundColor: Color = VCTheme.colors.background,
) = PointConfig(
guideline = guideline,
valueRange = valueRange,
hideValue = hideValue,
dotSize = pointSize,
focusedPointSize = focusedPointSize,
pointComparisonRule = pointComparisonRule,
strokeWidth = strokeWidth,
defaultColor = defaultColor,
pointColor = pointColor,
backgroundColor = backgroundColor
)
data class ChartBackgroundConfig(
val showBaseLine: Boolean,
val baseLineColor: Color,
val baseLineWidth: Dp,
val highlightingAreaYPos: Float,
val highlightingAreaHeight: Float,
val highlightingAreaColor: Color,
val highlightingLineColor: Color,
val highlightingLineWidth: Dp,
val highlightingLineAlignment: Alignment.Vertical
)
@Composable
fun createChartBackgroundConfig(
highlightingAreaHeight: Float,
showBaseLine: Boolean = false,
baseLineColor: Color = VCTheme.colors.graphLow,
baseLineWidth: Dp = 1.dp,
highlightingAreaYPos: Float = 0f,
highlightingAreaColor: Color = VCTheme.colors.graphSafety99,
highlightingLineColor: Color = VCTheme.colors.graphSafety10,
highlightingLineWidth: Dp = 1.dp,
highlightingLineAlignment: Alignment.Vertical = Alignment.Top
) = ChartBackgroundConfig(
showBaseLine = showBaseLine,
baseLineColor = baseLineColor,
baseLineWidth = baseLineWidth,
highlightingAreaYPos = highlightingAreaYPos,
highlightingAreaHeight = highlightingAreaHeight,
highlightingAreaColor = highlightingAreaColor,
highlightingLineColor = highlightingLineColor,
highlightingLineWidth = highlightingLineWidth,
highlightingLineAlignment = highlightingLineAlignment
)
@Composable
fun BoxScope.ChartBackground(
chartHeight: Float,
baselineYOffset: Float = 0f,
config: ChartBackgroundConfig,
chart: @Composable () -> Unit = {}
) {
if (config.highlightingAreaHeight != 0f) {
Canvas(
modifier = Modifier
.matchParentSize()
.offset(y = config.highlightingAreaYPos.toDp())
) {
drawRect(
color = config.highlightingAreaColor,
size = size.copy(height = config.highlightingAreaHeight)
)
}
}
chart()
if (config.highlightingAreaHeight != 0f) {
val highlightingLineYPos =
config.highlightingAreaYPos + if (config.highlightingLineAlignment == Alignment.Bottom) config.highlightingAreaHeight else 0f
Canvas(
modifier = Modifier
.matchParentSize()
.offset(y = highlightingLineYPos.toDp())
) {
drawHorizontalDash(
lineColor = config.highlightingLineColor,
xStartOffset = 0f,
xEndOffset = size.width,
yOffset = 0f,
strokeWidth = config.highlightingLineWidth.toPx(),
)
}
}
if (config.showBaseLine) {
Canvas(modifier = Modifier.matchParentSize()) {
drawHorizontalDash(
lineColor = config.baseLineColor,
xStartOffset = 0f,
xEndOffset = size.width,
yOffset = chartHeight + baselineYOffset,
strokeWidth = config.baseLineWidth.toPx()
)
}
}
}
data class DividerConfig(
val color: Color,
val width: Float
)
@Composable
fun NameTag(
items: List<Pair<String, Color>>,
groupingUnit: Int,
dividerConfig: DividerConfig
) {
val shouldGrouping = groupingUnit != 1
val step = if (shouldGrouping) items.size / groupingUnit else groupingUnit
val groupingIndices = (0..items.size step step)
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.Bottom
) {
items.forEachIndexed { index, item ->
if (groupingIndices.contains(index)) {
Text(
modifier = Modifier
.weight(1f)
.drawBehind {
drawVerticalDash(
lineColor = dividerConfig.color,
xOffset = 0f,
yTopOffset = 0f,
yBottomOffset = size.height,
strokeWidth = dividerConfig.width
)
}
.padding(top = 12.dp)
.wrapContentHeight(Alignment.Bottom)
.padding(horizontal = 4.dp),
text = item.first,
style = VCTheme.typo.caption1.copy(item.second),
textAlign = if (shouldGrouping) TextAlign.Start else TextAlign.Center,
maxLines = 2
)
}
}
}
}
data class FootNoteData(
val name: String,
val color: Color
)
@Composable
fun createFootNoteData(
name: String,
color: Color = VCTheme.colors.graphSafety10
) = FootNoteData(name = name, color = color)
@Composable
fun FootNote(footNoteData: FootNoteData) {
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
DashedHorizontalDivider(
modifier = Modifier
.width(16.dp)
.height(1.dp),
color = footNoteData.color
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = footNoteData.name,
style = VCTheme.typo.caption1.copy(footNoteData.color)
)
}
}
fun DrawScope.drawHorizontalDash(
lineColor: Color,
xStartOffset: Float,
xEndOffset: Float,
yOffset: Float,
strokeWidth: Float
) {
drawLine(
color = lineColor,
start = Offset(xStartOffset, yOffset),
end = Offset(xEndOffset, yOffset),
strokeWidth = strokeWidth,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(2.dp.toPx(), 2.dp.toPx(), 0f))
)
}
fun DrawScope.drawVerticalDash(
lineColor: Color,
xOffset: Float,
yTopOffset: Float,
yBottomOffset: Float,
strokeWidth: Float
) {
drawLine(
color = lineColor,
start = Offset(xOffset, yTopOffset),
end = Offset(xOffset, yBottomOffset),
strokeWidth = strokeWidth,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(2.dp.toPx(), 2.dp.toPx(), 0f))
)
}
data class Area(val min: Float, val max: Float, val color: Color)
fun createShaderBrush(
size: Size,
areas: List<Area>,
): ShaderBrush {
val bitmapCanvas = BitmapCanvas(width = size.width.toInt(), height = size.height.toInt())
val canvas = android.graphics.Canvas(bitmapCanvas.bitmap)
areas.forEach { area ->
val paint = Paint().apply {
color = area.color.toArgb()
}
canvas.drawRect(RectF(0f, area.max, size.width, area.min), paint)
}
return ShaderBrush(ImageShader(bitmapCanvas.imageBitmap))
}
@Preview
@Composable
private fun LineChart_Preview_CV() {
VirtualCareTheme(darkTheme = true) {
val stringFormat = "%s%%"
PointLineChart(
state = rememberLineChartState(
points = listOf(
LineChartPoint(
value = 10.7f,
valueDisplay = (10.7f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(10.7f.toIntegerableString()),
name = "수",
detailName = "10.13"
),
LineChartPoint(
value = 5.8f,
valueDisplay = (5.8f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(5.8f.toIntegerableString()),
name = "목",
detailName = "10.14"
),
LineChartPoint(
value = 120f,
valueDisplay = "높음",
valueDetailDisplay = stringFormat.format(120f.toIntegerableString()),
name = "금",
detailName = "10.15"
),
LineChartPoint(
value = 33f,
valueDisplay = (33f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(33f.toIntegerableString()),
name = "토",
detailName = "10.16"
),
LineChartPoint(
value = -10f,
valueDisplay = "낮음",
valueDetailDisplay = stringFormat.format((-10f).toIntegerableString()),
name = "일",
detailName = "10.17"
),
LineChartPoint(
value = 33.1f,
valueDisplay = (33.1f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(33.1f.toIntegerableString()),
name = "월",
detailName = "10.18"
),
LineChartPoint(
value = 36f,
valueDisplay = (36f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(36f.toIntegerableString()),
name = "화",
detailName = "화"
),
),
pointConfig = createPointConfig(
guideline = Range(min = 0f, max = 36f),
valueRange = Range(min = 0f, max = 100f),
pointComparisonRule = ComparisonRule.Under
),
groupingUnit = 1,
)
)
}
}
@Preview
@Composable
private fun PointLineChart_Preview_CV2() {
VirtualCareTheme(darkTheme = true) {
val stringFormat = "%3.1f%%"
PointLineChart(
state = rememberPointLineChartState(
points = listOf(
LineChartPoint(
value = null,
valueDisplay = "",
valueDetailDisplay = "",
name = "18",
detailName = "10.18"
),
LineChartPoint(
value = null,
valueDisplay = "",
valueDetailDisplay = "",
name = "19",
detailName = "10.19"
),
LineChartPoint(
value = null,
valueDisplay = "",
valueDetailDisplay = "",
name = "20",
detailName = "10.20"
),
LineChartPoint(
value = 27f,
valueDisplay = (27f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(27f),
name = "21",
detailName = "10.21"
),
LineChartPoint(
value = 34f,
valueDisplay = (34f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(34f),
name = "22",
detailName = "10.22"
),
LineChartPoint(
value = 30f,
valueDisplay = (30f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(30f),
name = "23",
detailName = "10.23"
),
LineChartPoint(
value = 15f,
valueDisplay = (15f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(15f),
name = "24",
detailName = "10.24"
),
LineChartPoint(
value = 34f,
valueDisplay = (34f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(34f),
name = "25",
detailName = "10.25"
),
LineChartPoint(
value = 37f,
valueDisplay = (37f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(37f),
name = "26",
detailName = "10.26"
),
LineChartPoint(
value = null,
valueDisplay = "",
valueDetailDisplay = "",
name = "27",
detailName = "10.27"
),
LineChartPoint(
value = null,
valueDisplay = "",
valueDetailDisplay = "",
name = "28",
detailName = "10.28"
),
LineChartPoint(
value = null,
valueDisplay = "",
valueDetailDisplay = "",
name = "29",
detailName = "10.29"
),
LineChartPoint(
value = 30f,
valueDisplay = (30f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(30f),
name = "30",
detailName = "10.30"
),
LineChartPoint(
value = 13f,
valueDisplay = (13f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(13f),
name = "31",
detailName = "10.31"
),
LineChartPoint(
value = 37f,
valueDisplay = (37f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(37f),
name = "1",
detailName = "11.1"
),
LineChartPoint(
value = 39f,
valueDisplay = (39f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(39f),
name = "2",
detailName = "11.1"
),
LineChartPoint(
value = 37f,
valueDisplay = (37f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(37f),
name = "3",
detailName = "11.3"
),
LineChartPoint(
value = 24f,
valueDisplay = (24f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(24f),
name = "4",
detailName = "11.4"
),
LineChartPoint(
value = 30f,
valueDisplay = (30f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(30f),
name = "5",
detailName = "11.5"
),
LineChartPoint(
value = 48f,
valueDisplay = (48f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(48f),
name = "6",
detailName = "11.6"
),
LineChartPoint(
value = 8f,
valueDisplay = (8f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(8f),
name = "7",
detailName = "11.7"
),
LineChartPoint(
value = 34f,
valueDisplay = (34f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(34f),
name = "8",
detailName = "11.8"
),
LineChartPoint(
value = 37f,
valueDisplay = (37f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(37f),
name = "9",
detailName = "11.9"
),
LineChartPoint(
value = 34f,
valueDisplay = (34f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(34f),
name = "10",
detailName = "11.10"
),
LineChartPoint(
value = 39f,
valueDisplay = (39f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(39f),
name = "11",
detailName = "11.11"
),
LineChartPoint(
value = 52f,
valueDisplay = (52f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(52f),
name = "12",
detailName = "11.12"
),
LineChartPoint(
value = 23f,
valueDisplay = (23f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(23f),
name = "13",
detailName = "11.13"
),
LineChartPoint(
value = 30f,
valueDisplay = (30f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(30f),
name = "14",
detailName = "11.14"
),
LineChartPoint(
value = 29f,
valueDisplay = (29f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(29f),
name = "15",
detailName = "11.15"
),
LineChartPoint(
value = 30f,
valueDisplay = (30f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(30f),
name = "16",
detailName = "11.16"
),
),
pointConfig = createPointConfig(
guideline = Range(min = 0f, max = 36f),
valueRange = Range(min = 0f, max = 100f),
pointComparisonRule = ComparisonRule.Under,
hideValue = true,
),
groupingUnit = 5,
)
)
}
}
@Preview
@Composable
private fun LineChart_Preview_MEAN() {
VirtualCareTheme(darkTheme = true) {
val stringFormat = "%3.1f%%"
PointLineChart(
state = rememberLineChartState(
points = listOf(
LineChartPoint(
value = 102f,
valueDisplay = (102f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(102f),
name = "수",
detailName = "10.13"
),
LineChartPoint(
value = 117f,
valueDisplay = (117f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(117f),
name = "목",
detailName = "10.14"
),
LineChartPoint(
value = 164f,
valueDisplay = (164f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(164f),
name = "금",
detailName = "10.15"
),
LineChartPoint(
value = 126f,
valueDisplay = (126f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(126f),
name = "토",
detailName = "10.16"
),
LineChartPoint(
value = 145f,
valueDisplay = (145f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(145f),
name = "일",
detailName = "10.17"
),
LineChartPoint(
value = 110f,
valueDisplay = (110f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(110f),
name = "월",
detailName = "10.18"
),
LineChartPoint(
value = 70f,
valueDisplay = (70f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(70f),
name = "화",
detailName = "화"
),
),
pointConfig = createPointConfig(
guideline = Range(min = 70f, max = 117f),
valueRange = Range(70f, 164f),
pointComparisonRule = ComparisonRule.Below
),
groupingUnit = 1,
)
)
}
}
@Preview
@Composable
private fun LineChart_Preview_MEAN2() {
VirtualCareTheme(darkTheme = true) {
val stringFormat = "%3f%%"
PointLineChart(
state = rememberLineChartState(
points = listOf(
LineChartPoint(
value = 239f,
valueDisplay = (239f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(239f),
name = "수",
detailName = "10.13"
),
LineChartPoint(
value = 300f,
valueDisplay = (300f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(300f),
name = "목",
detailName = "10.14"
),
LineChartPoint(
value = 330f,
valueDisplay = (330f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(330f),
name = "금",
detailName = "10.15"
),
LineChartPoint(
value = 69f,
valueDisplay = (69f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(69f),
name = "토",
detailName = "10.16"
),
LineChartPoint(
value = 169f,
valueDisplay = (169f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(169f),
name = "일",
detailName = "10.17"
),
LineChartPoint(
value = 141f,
valueDisplay = (141f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(141f),
name = "월",
detailName = "10.18"
),
LineChartPoint(
value = 40f,
valueDisplay = (40f).toIntegerableString(),
valueDetailDisplay = stringFormat.format(40f),
name = "화",
detailName = "화"
),
),
pointConfig = createPointConfig(
guideline = Range(min = 70f, max = 154f),
valueRange = Range(min = 40f, max = 300f),
pointComparisonRule = ComparisonRule.Below
),
groupingUnit = 1,
title = "평균 혈당"
)
)
}
}
@Preview
@Composable
private fun LineChart_Preview_GMI() {
VirtualCareTheme(darkTheme = true) {
PointLineChart(
state = rememberLineChartState(
points = listOf(
LineChartPoint(
value = 7f,
valueDisplay = 7f.toIntegerableString() + "%",
name = "1차\n10.13",
detailName = "10.13"
)
),
groupingUnit = 1,
pointConfig = createPointConfig(
valueRange = Range(min = 0f, max = 14f),
defaultColor = VCTheme.colors.onSurface0
)
)
)
}
}
@Preview
@Composable
private fun LineChart_Preview_GMI2() {
VirtualCareTheme(darkTheme = true) {
PointLineChart(
state = rememberLineChartState(
points = listOf(
LineChartPoint(
value = 7f,
valueDisplay = 7f.toIntegerableString() + "%",
name = "1차\n10.13",
detailName = "10.13"
), LineChartPoint(
value = 7f,
valueDisplay = 7f.toIntegerableString() + "%",
name = "1차\n10.13",
detailName = "10.13"
), LineChartPoint(
value = 7f,
valueDisplay = 7f.toIntegerableString() + "%",
name = "1차\n10.13",
detailName = "10.13"
), LineChartPoint(
value = 7f,
valueDisplay = 7f.toIntegerableString() + "%",
name = "1차\n10.13",
detailName = "10.13"
)
),
groupingUnit = 1,
pointConfig = createPointConfig(
valueRange = Range(min = 0f, max = 14f),
defaultColor = VCTheme.colors.onSurface0
)
)
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment