Skip to content

Instantly share code, notes, and snippets.

@kimji1
Last active February 24, 2025 15:39
Show Gist options
  • Save kimji1/ffdf7edc46b7323f0fd19d9ea6ae33b8 to your computer and use it in GitHub Desktop.
Save kimji1/ffdf7edc46b7323f0fd19d9ea6ae33b8 to your computer and use it in GitHub Desktop.
@Composable
fun VerticalStackedBarChart(modifier: Modifier = Modifier, state: VerticalStackedBarChartState) {
var parentWidth by remember { mutableFloatStateOf(0f) }
val mutableItems: MutableList<VerticalStackedBar> = remember(state.items) {
state.items.toMutableStateList()
}
val barHeight = state.barHeight.toPx()
val chartUnit = barHeight / 100
val gap = state.gap.toPx()
val barCornerRadius = state.barCornerRadius.toPx()
val underHeight = mutableItems.maxOfOrNull { bar ->
bar.elements.filter { it.isLower }.mapNotNull { it.value }.sum() * chartUnit
} ?: 0f
val chartContentHeight = barHeight.toDp() + underHeight.toDp()
val showBaseLine = mutableItems.flatMap { bar ->
bar.elements.filter { it.isLower && it.value != null && it.value > 0f }
}.isNotEmpty()
var chartSize by remember { mutableStateOf(Size(0f, 0f)) }
val barItemWidthState = remember { mutableFloatStateOf(0f) }
var focusedItem: VerticalStackedBar? by remember(state.items) { mutableStateOf(null) }
var popupSize by remember { mutableStateOf(Size(0f, 0f)) }
var popupXOffset by remember { mutableFloatStateOf(0f) }
var popupYOffset by remember { mutableFloatStateOf(0f) }
val popupMargin = 4.dp.toPx()
var showDetailPopup by remember { mutableStateOf(false) }
LaunchedEffect(focusedItem, popupSize, chartSize) {
if (focusedItem == null || popupSize.height == 0f || chartSize.height == 0f || chartSize.width == 0f) return@LaunchedEffect
if (focusedItem?.focused == false) {
showDetailPopup = false
return@LaunchedEffect
}
val index = mutableItems.indexOf(focusedItem)
val isLeftSideBar = index <= (mutableItems.lastIndex / 2f)
val itemWidth = chartSize.width / mutableItems.size
val barWidth = itemWidth / 3f
popupXOffset = if (isLeftSideBar) {
val xPos = (itemWidth * index) + (barWidth * 2) + popupMargin
val screenOffWidth = xPos + popupSize.width - parentWidth
if ((xPos + popupSize.width > parentWidth)) xPos - screenOffWidth else xPos
} else {
val xPos = (itemWidth * index) + barWidth - popupSize.width - popupMargin
if (xPos < 0f) 0f else xPos
}
popupYOffset = (chartSize.height - popupSize.height) / 2
showDetailPopup = focusedItem?.focused == true
}
fun changeFocused(item: VerticalStackedBar) {
val index = mutableItems.indexOf(item)
mutableItems.replaceAll { it.copy(focused = false) }
mutableItems[index] = mutableItems[index].copy(focused = !item.focused)
focusedItem = if (mutableItems[index].focused) mutableItems[index] else null
popupXOffset = Float.MAX_VALUE
}
Box(modifier = modifier.onSizeChanged { parentWidth = it.width.toFloat() }) {
Column(modifier = Modifier.align(Alignment.TopCenter)) {
Box {
ChartBackground(
chartHeight = barHeight,
baselineYOffset = gap,
config = state.chartBackgroundConfig.copy(showBaseLine = showBaseLine)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(chartContentHeight)
.onSizeChanged { chartSize = it.toSize() }
) {
mutableItems.forEach { item ->
VerticalStackedBar(
item = item,
barHeight = barHeight,
barWidthState = barItemWidthState,
chartUnit = chartUnit,
barCornerRadius = barCornerRadius,
gap = gap,
dividerConfig = state.dividerConfig,
changeFocused = ::changeFocused
)
}
}
}
}
HorizontalDivider(
modifier = Modifier.padding(top = if (underHeight != 0f) 8.dp else 0.dp),
thickness = 1.dp,
color = VCTheme.colors.secondaryLine
)
NameTag(
items = mutableItems.map { it.name to it.nameColor },
groupingUnit = state.groupingUnit,
dividerConfig = state.dividerConfig
)
if (state.footNoteData != null) {
FootNote(footNoteData = state.footNoteData)
}
}
if (focusedItem != null) {
val item = focusedItem ?: return@Box
StackedBarDetailPopup(
modifier = Modifier
.onSizeChanged {
popupSize = Size(it.width.toFloat(), it.height.toFloat())
}
.offset(x = popupXOffset.toDp(), y = popupYOffset.toDp())
.clip(shape = RoundedCornerShape(12.dp))
.noRippleClickable { changeFocused(item) }
.widthIn(max = parentWidth.toDp())
.alpha(if (showDetailPopup && popupXOffset != Float.MAX_VALUE) 1f else 0f),
focusedItem = item
)
}
}
}
@Composable
fun StackedBarDetailPopup(
modifier: Modifier = Modifier,
focusedItem: VerticalStackedBar
) {
if (focusedItem.elements.mapNotNull { it.value }.isEmpty()) {
return
}
Box(
modifier = modifier
.wrapContentHeight(unbounded = true)
.wrapContentWidth()
.border(
width = 1.dp,
color = VCTheme.colors.onSurface70,
shape = RoundedCornerShape(12.dp)
)
.background(VCTheme.colors.surface99, shape = RoundedCornerShape(12.dp))
.padding(horizontal = 12.dp, vertical = 8.dp)
) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
var height by remember { mutableFloatStateOf(0f) }
Row(
modifier = Modifier
.wrapContentWidth()
.onSizeChanged { height = it.height.toFloat() },
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Column(
modifier = Modifier.heightIn(min = height.toDp()),
verticalArrangement = Arrangement.SpaceBetween
) {
focusedItem.elements.forEach { element ->
val color = if (element.value != null && element.value > 0f) {
element.nameColor
} else VCTheme.colors.onSurface70
Text(
text = element.name,
style = VCTheme.typo.subhead.copy(color = color)
)
}
}
Column(
modifier = Modifier.heightIn(min = height.toDp()),
verticalArrangement = Arrangement.SpaceBetween
) {
focusedItem.elements.forEach { element ->
val color = if ((element.value ?: 0f) > 0f) element.nameColor else VCTheme.colors.onSurface70
Text(
modifier = Modifier.weight(1f, false),
text = element.valueStr,
style = VCTheme.typo.subhead.copy(color = color)
)
}
}
}
Text(
text = focusedItem.detailName,
style = VCTheme.typo.caption1B.copy(color = VCTheme.colors.onSurface0)
)
}
}
}
@Composable
fun RowScope.VerticalStackedBar(
modifier: Modifier = Modifier,
item: VerticalStackedBar?,
barHeight: Float,
chartUnit: Float,
barCornerRadius: Float,
gap: Float,
dividerConfig: DividerConfig,
changeFocused: (selectedItem: VerticalStackedBar) -> Unit,
barWidthState: MutableFloatState
) {
Canvas(
modifier = modifier
.weight(1f)
.fillMaxHeight()
.conditional(item != null) {
noRippleClickable {
item?.let { changeFocused(it) }
}
}
) {
barWidthState.floatValue = size.width
if (item != null) {
val height = drawVerticalStackedBar(
barHeight = barHeight,
chartUnit = chartUnit,
radius = barCornerRadius,
gap = gap,
item = item,
)
drawVerticalDash(
lineColor = dividerConfig.color,
xOffset = 0f,
yTopOffset = 0f,
yBottomOffset = height,
strokeWidth = dividerConfig.width
)
}
}
}
fun DrawScope.drawVerticalStackedBar(
barHeight: Float,
chartUnit: Float,
radius: Float,
gap: Float,
item: VerticalStackedBar,
): Float {
if (item.elements.mapNotNull { it.value }.isEmpty()) {
return 0f
}
val cornerRadius = CornerRadius(radius, radius)
val zeroRadius = CornerRadius(0f, 0f)
val firstIndex = item.elements.indexOfFirst { it.value != null && it.value > 0f }
val lastIndex = item.elements.indexOfLast { it.value != null && it.value > 0f }
val defaultBarWidth = size.width / 3f
val upperItemHeight =
(item.elements.filter { it.isLower.not() }.mapNotNull { it.value }.sum() * chartUnit)
val topOffset = barHeight - upperItemHeight
var stackedHeight = 0f
var totalRatio = 100f
item.elements.forEachIndexed { index, element ->
if (element.value == null || element.value == 0f) return@forEachIndexed
val containedNextGap =
element.isLower && index != lastIndex && item.elements.getOrNull(index + 1)?.value == 1f
val unit = (barHeight - stackedHeight) / totalRatio
val height = if (element.value == 1f) { // 1% 인 경우 비율에 따른 높이가 아닌 1dp 고정값
1.dp.toPx()
} else if (element.value > 0f && element.isUpper || element.isLower) {
(unit * element.value) - if (containedNextGap) gap * 2 else gap
} else if (element.value > 0f) {
unit * element.value
} else 0f
val barWidth =
if (element.value == 1f && (index == firstIndex || index == lastIndex)) defaultBarWidth * 0.9f else defaultBarWidth
val path = Path().apply {
addRoundRect(
RoundRect(
rect = Rect(
offset = Offset(
x = defaultBarWidth + (defaultBarWidth - barWidth) / 2,
y = topOffset + stackedHeight + if (element.isLower && element.value > 0f) gap else 0f
),
size = Size(width = barWidth, height = height),
),
topLeft = if (index == firstIndex) cornerRadius else zeroRadius,
topRight = if (index == firstIndex) cornerRadius else zeroRadius,
bottomLeft = if (index == lastIndex && (element.isUpper || element.isLower)) cornerRadius else zeroRadius,
bottomRight = if (index == lastIndex && (element.isUpper || element.isLower)) cornerRadius else zeroRadius
)
)
}
drawPath(path, color = if (item.focused) element.focusedColor else element.color)
stackedHeight += if (element.value > 0f && (element.isUpper || element.isLower)) {
height + gap
} else if (element.value > 0f) {
height
} else 0f
totalRatio -= element.value
}
return (topOffset + stackedHeight)
}
@Preview
@Composable
fun VerticalStackedBarChartPreview_1() {
VirtualCareTheme(darkTheme = true) {
val firstElement = VerticalStackedBarElement(
name = "매우 높음",
value = 0f,
valueStr = "",
color = VCTheme.colors.graphHigh80,
focusedColor = VCTheme.colors.graphHigh40,
nameColor = VCTheme.colors.graphHigh40,
isUpper = true
)
val secondElement = VerticalStackedBarElement(
name = "높음",
value = 0f,
valueStr = "",
color = VCTheme.colors.graphHigh90,
focusedColor = VCTheme.colors.graphHigh,
nameColor = VCTheme.colors.graphHigh,
isUpper = true
)
val thirdElement = VerticalStackedBarElement(
name = "목표 범위",
value = 0f,
valueStr = "",
color = VCTheme.colors.graphSafety40,
focusedColor = VCTheme.colors.graphSafety40,
nameColor = VCTheme.colors.graphSafety10,
)
val forthElement = VerticalStackedBarElement(
name = "낮음",
value = 0f,
valueStr = "",
color = VCTheme.colors.graphLow80,
focusedColor = VCTheme.colors.graphLow,
nameColor = VCTheme.colors.graphLow,
isLower = true
)
val fifthElement = VerticalStackedBarElement(
name = "매우 낮음",
value = 0f,
valueStr = "",
color = VCTheme.colors.graphLow90,
focusedColor = VCTheme.colors.graphLow40,
nameColor = VCTheme.colors.graphLow40,
isLower = true,
)
VerticalStackedBarChart(
state = rememberVerticalStackedBarChartState(
footNoteData = FootNoteData("70% 이상 권장", VCTheme.colors.graphSafety10),
items = listOf(
VerticalStackedBar(
elements = listOf(
firstElement.copy(value = 1f, valueStr = "1%"),
secondElement.copy(value = 1f, valueStr = "1%"),
thirdElement.copy(value = 96f, valueStr = "96%"),
forthElement.copy(value = 1f, valueStr = "1%"),
fifthElement.copy(value = 1f, valueStr = "1%")
),
name = "화",
detailName = "10.17",
nameColor = VCTheme.colors.onSurface0
),
VerticalStackedBar(
elements = listOf(
firstElement.copy(value = 0f, valueStr = "0%"),
secondElement.copy(value = 0f, valueStr = "0%"),
thirdElement.copy(value = 20f, valueStr = "20%"),
forthElement.copy(value = 3f, valueStr = "3%"),
fifthElement.copy(value = 77f, valueStr = "77%")
),
name = "월",
detailName = "10.17",
nameColor = VCTheme.colors.onSurface0
),
VerticalStackedBar(
elements = listOf(
firstElement.copy(value = 0f, valueStr = "0%"),
secondElement.copy(value = 0f, valueStr = "0%"),
thirdElement.copy(value = 0f, valueStr = "0%"),
forthElement.copy(value = 48f, valueStr = "48%"),
fifthElement.copy(value = 52f, valueStr = "52%")
),
name = "일",
detailName = "10.17",
nameColor = VCTheme.colors.error
),
VerticalStackedBar(
elements = listOf(
firstElement.copy(value = 0f, valueStr = "0%"),
secondElement.copy(value = 0f, valueStr = "0%"),
thirdElement.copy(value = 0f, valueStr = "0%"),
forthElement.copy(value = 0f, valueStr = "0%"),
fifthElement.copy(value = 100f, valueStr = "100%")
),
name = "토",
detailName = "10.17",
nameColor = VCTheme.colors.onSurface0
),
VerticalStackedBar(
elements = listOf(
firstElement.copy(value = 0f, valueStr = "0%"),
secondElement.copy(value = 1f, valueStr = "1%"),
thirdElement.copy(value = 24f, valueStr = "24%"),
forthElement.copy(value = 46.5f, valueStr = "46%"),
fifthElement.copy(value = 28.5f, valueStr = "29%")
),
name = "금",
detailName = "10.17",
nameColor = VCTheme.colors.onSurface0
),
VerticalStackedBar(
elements = listOf(
firstElement.copy(value = 1f, valueStr = "1%"),
secondElement.copy(value = 29f, valueStr = "29%"),
thirdElement.copy(value = 50f, valueStr = "50%"),
forthElement.copy(value = 19f, valueStr = "19%"),
fifthElement.copy(value = 1f, valueStr = "1%")
),
name = "목",
detailName = "10.17",
nameColor = VCTheme.colors.onSurface0
),
VerticalStackedBar(
elements = listOf(
firstElement.copy(value = 14f, valueStr = "14%"),
secondElement.copy(value = 28f, valueStr = "28%"),
thirdElement.copy(value = 7f, valueStr = "7%"),
forthElement.copy(value = 19f, valueStr = "19%"),
fifthElement.copy(value = 32f, valueStr = "32%")
),
name = "수",
detailName = "10.17",
nameColor = VCTheme.colors.onSurface0,
focused = true
)
)
)
)
}
}
@Preview
@Composable
private fun VerticalStackedBarChartPreview_2() {
VirtualCareTheme(darkTheme = true) {
val firstElement = VerticalStackedBarElement(
name = "매우 높음",
value = 0f,
valueStr = "0%",
color = VCTheme.colors.graphHigh80,
focusedColor = VCTheme.colors.graphHigh40,
nameColor = VCTheme.colors.graphHigh40,
isUpper = true
)
val secondElement = VerticalStackedBarElement(
name = "높음",
value = 0f,
valueStr = "0%",
color = VCTheme.colors.graphHigh90,
focusedColor = VCTheme.colors.graphHigh,
nameColor = VCTheme.colors.graphHigh,
isUpper = true
)
val thirdElement = VerticalStackedBarElement(
name = "목표 범위",
value = 100f,
valueStr = "100%",
color = VCTheme.colors.graphSafety40,
focusedColor = VCTheme.colors.graphSafety40,
nameColor = VCTheme.colors.graphSafety10,
)
val forthElement = VerticalStackedBarElement(
name = "낮음",
value = 0f,
valueStr = "0%",
color = VCTheme.colors.graphLow80,
focusedColor = VCTheme.colors.graphLow,
nameColor = VCTheme.colors.graphLow,
isLower = true
)
val fifthElement = VerticalStackedBarElement(
name = "매우 낮음",
value = 0f,
valueStr = "0%",
color = VCTheme.colors.graphLow90,
focusedColor = VCTheme.colors.graphLow40,
nameColor = VCTheme.colors.graphLow40,
isLower = true,
)
VerticalStackedBarChart(
state = rememberVerticalStackedBarChartState(
footNoteData = FootNoteData("70% 이상 권장", VCTheme.colors.graphSafety10),
items = listOf(
VerticalStackedBar(
elements = listOf(
firstElement,
secondElement,
thirdElement,
forthElement,
fifthElement
),
name = "화",
detailName = "10.17",
nameColor = VCTheme.colors.onSurface0
),
VerticalStackedBar(
elements = listOf(
firstElement,
secondElement,
thirdElement,
forthElement,
fifthElement
),
name = "월",
detailName = "10.17",
nameColor = VCTheme.colors.onSurface0
),
VerticalStackedBar(
elements = listOf(
firstElement,
secondElement,
thirdElement,
forthElement,
fifthElement
),
name = "일",
detailName = "10.17",
nameColor = VCTheme.colors.error
),
VerticalStackedBar(
elements = listOf(
firstElement,
secondElement,
thirdElement,
forthElement,
fifthElement
),
name = "토",
detailName = "10.17",
nameColor = VCTheme.colors.onSurface0,
focused = true
),
VerticalStackedBar(
elements = listOf(
firstElement.copy(value = 3f, valueStr = "3%"),
secondElement.copy(value = 23f, valueStr = "23%"),
thirdElement.copy(value = 74f, valueStr = "74%"),
forthElement.copy(value = 0f, valueStr = "0%"),
fifthElement.copy(value = 0f, valueStr = "0%")
),
name = "금",
detailName = "10.17",
nameColor = VCTheme.colors.onSurface0
),
VerticalStackedBar(
elements = listOf(
firstElement.copy(value = 0f, valueStr = "0%"),
secondElement.copy(value = 0f, valueStr = "0%"),
thirdElement.copy(value = 20f, valueStr = "20%"),
forthElement.copy(value = 3f, valueStr = "3%"),
fifthElement.copy(value = 77f, valueStr = "77%")
),
name = "목",
detailName = "10.17",
nameColor = VCTheme.colors.onSurface0
),
VerticalStackedBar(
elements = listOf(
firstElement,
secondElement,
thirdElement,
forthElement,
fifthElement
),
name = "수",
detailName = "10.17",
nameColor = VCTheme.colors.onSurface0,
)
)
)
)
}
}
@Preview
@Composable
private fun VerticalStackedBarChartPreview_3() {
VirtualCareTheme(darkTheme = true) {
val firstElement = VerticalStackedBarElement(
name = "매우 높음",
value = 0f,
valueStr = "0%",
color = VCTheme.colors.graphHigh80,
focusedColor = VCTheme.colors.graphHigh40,
nameColor = VCTheme.colors.graphHigh40,
isUpper = true
)
val secondElement = VerticalStackedBarElement(
name = "높음",
value = 0f,
valueStr = "0%",
color = VCTheme.colors.graphHigh90,
focusedColor = VCTheme.colors.graphHigh,
nameColor = VCTheme.colors.graphHigh,
isUpper = true
)
val thirdElement = VerticalStackedBarElement(
name = "목표 범위",
value = 0f,
valueStr = "0%",
color = VCTheme.colors.graphSafety40,
focusedColor = VCTheme.colors.graphSafety40,
nameColor = VCTheme.colors.graphSafety10,
)
val forthElement = VerticalStackedBarElement(
name = "낮음",
value = 0f,
valueStr = "0%",
color = VCTheme.colors.graphLow80,
focusedColor = VCTheme.colors.graphLow,
nameColor = VCTheme.colors.graphLow,
isLower = true
)
val fifthElement = VerticalStackedBarElement(
name = "매우 낮음",
value = 0f,
valueStr = "0%",
color = VCTheme.colors.graphLow90,
focusedColor = VCTheme.colors.graphLow40,
nameColor = VCTheme.colors.graphLow40,
isLower = true,
)
val items = listOf(
VerticalStackedBar(
elements = listOf(
firstElement.copy(value = 0f, valueStr = "0%"),
secondElement.copy(value = 1f, valueStr = "1%"),
thirdElement.copy(value = 24f, valueStr = "24%"),
forthElement.copy(value = 46.5f, valueStr = "46%"),
fifthElement.copy(value = 28.5f, valueStr = "29%")
),
name = "화",
detailName = "10.17",
nameColor = VCTheme.colors.onSurface0
),
VerticalStackedBar(
elements = listOf(
firstElement.copy(value = 1f, valueStr = "1%"),
secondElement.copy(value = 29f, valueStr = "29%"),
thirdElement.copy(value = 50f, valueStr = "50%"),
forthElement.copy(value = 19f, valueStr = "19%"),
fifthElement.copy(value = 1f, valueStr = "1%")
),
name = "월",
detailName = "10.17",
nameColor = VCTheme.colors.onSurface0
),
VerticalStackedBar(
elements = listOf(
firstElement.copy(value = 0f, valueStr = "0%"),
secondElement.copy(value = 30f, valueStr = "30%"),
thirdElement.copy(value = 50f, valueStr = "50%"),
forthElement.copy(value = 20f, valueStr = "20%"),
fifthElement.copy(value = 0f, valueStr = "0%")
),
name = "일",
detailName = "10.17",
nameColor = VCTheme.colors.error
),
VerticalStackedBar(
elements = listOf(
firstElement.copy(value = 1f, valueStr = "1%"),
secondElement.copy(value = 29f, valueStr = "29%"),
thirdElement.copy(value = 50f, valueStr = "50%"),
forthElement.copy(value = 19f, valueStr = "19%"),
fifthElement.copy(value = 1f, valueStr = "1%")
),
name = "토",
detailName = "10.17",
nameColor = VCTheme.colors.onSurface0
),
VerticalStackedBar(
elements = listOf(
firstElement.copy(value = 0f, valueStr = "0%"),
secondElement.copy(value = 1f, valueStr = "1%"),
thirdElement.copy(value = 24f, valueStr = "24%"),
forthElement.copy(value = 46.5f, valueStr = "46%"),
fifthElement.copy(value = 28.5f, valueStr = "29%")
),
name = "금",
detailName = "10.17",
nameColor = VCTheme.colors.onSurface0
),
VerticalStackedBar(
elements = listOf(
firstElement.copy(value = 1f, valueStr = "1%"),
secondElement.copy(value = 29f, valueStr = "29%"),
thirdElement.copy(value = 50f, valueStr = "50%"),
forthElement.copy(value = 19f, valueStr = "19%"),
fifthElement.copy(value = 1f, valueStr = "1%")
),
name = "목",
detailName = "10.17",
nameColor = VCTheme.colors.onSurface0
),
VerticalStackedBar(
elements = listOf(
firstElement.copy(value = 14f, valueStr = "14%"),
secondElement.copy(value = 28f, valueStr = "28%"),
thirdElement.copy(value = 7f, valueStr = "7%"),
forthElement.copy(value = 19f, valueStr = "19%"),
fifthElement.copy(value = 32f, valueStr = "32%")
),
name = "수",
detailName = "10.17",
nameColor = VCTheme.colors.onSurface0,
focused = false
)
)
VerticalStackedBarChart(
state = rememberVerticalStackedBarChartState(
footNoteData = FootNoteData("70% 이상 권장", VCTheme.colors.graphSafety10),
items = (items + items + items + items + items).take(30),
groupingUnit = 5
)
)
}
}
data class VerticalStackedBarChartState(
val items: List<VerticalStackedBar>,
val groupingUnit: Int,
val barHeight: Dp,
val barWidth: Dp,
val barCornerRadius: Dp,
val gap: Dp,
val dividerConfig: DividerConfig,
val chartBackgroundConfig: ChartBackgroundConfig,
val footNoteData: FootNoteData?
)
data class VerticalStackedBar(
val elements: List<VerticalStackedBarElement>,
val name: String,
val detailName: String,
val nameColor: Color,
var focused: Boolean = false
)
data class VerticalStackedBarElement(
val name: String,
val value: Float?,
val valueStr: String,
val color: Color,
val focusedColor: Color,
val nameColor: Color,
val isUpper: Boolean = false,
val isLower: Boolean = false
)
@Composable
fun rememberVerticalStackedBarChartState(
items: List<VerticalStackedBar>,
barHeight: Dp = 200.dp,
barWidth: Dp = 16.dp,
gap: Dp = 2.dp,
barCornerRadius: Dp = 4.dp,
groupingUnit: Int = 1,
dividerConfig: DividerConfig = DividerConfig(
color = VCTheme.colors.secondaryLine,
width = 1.dp.toPx()
),
highlightingAreaRatio: Float = 0.3f,
chartBackgroundConfig: ChartBackgroundConfig = createChartBackgroundConfig(
highlightingAreaHeight = barHeight.toPx() * highlightingAreaRatio,
highlightingLineAlignment = Alignment.Bottom
),
footNoteData: FootNoteData? = null
) = remember(items) {
VerticalStackedBarChartState(
items = items,
barHeight = barHeight,
barWidth = barWidth,
gap = gap,
barCornerRadius = barCornerRadius,
groupingUnit = groupingUnit,
dividerConfig = dividerConfig,
chartBackgroundConfig = chartBackgroundConfig,
footNoteData = footNoteData,
)
}
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))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment