Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package codegen.histogram

import codegen.common.BaseChartCodeGenerator
import codegen.common.buildChartImports
import codegen.common.resolveStyleArguments
import model.BarPointInput
import model.ChartCodeGenerator
import model.GeneratedSnippet
import model.HistogramCodegenConfig

class HistogramChartCodeGenerator :
BaseChartCodeGenerator<HistogramCodegenConfig>(),
ChartCodeGenerator<HistogramCodegenConfig> {
override fun generate(config: HistogramCodegenConfig): GeneratedSnippet {
val items = normalizePoints(config.points)
val styleArguments = resolveStyleArguments(config.styleProperties, config.codegenMode)
val includeStyle = styleArguments.isNotEmpty()
val imports =
buildChartImports(
baseImports = BASE_IMPORTS,
styleImport = if (includeStyle) STYLE_IMPORT else null,
styleArguments = styleArguments,
)
val bodyLines = mutableListOf<String>()
bodyLines += buildDataSetCode(items, config.title)
bodyLines += ""

if (includeStyle) {
bodyLines += buildStyleCode(STYLE_BUILDER, styleArguments)
bodyLines += ""
}

bodyLines += buildChartCallCode(COMPONENT_NAME, includeStyle)
val code = buildFunctionCode(imports, config.functionName, bodyLines)
return GeneratedSnippet(code = code)
}

private fun normalizePoints(points: List<BarPointInput>): List<NormalizedItem> =
points.mapIndexed { index, point ->
val sanitizedLabel = point.label.trim().ifBlank { "Bin ${index + 1}" }
val floatValue = point.valueText.toFloatOrNull()?.coerceAtLeast(0f) ?: 0f
NormalizedItem(
label = sanitizedLabel,
value = floatValue,
)
}

private companion object {
val BASE_IMPORTS =
listOf(
"import androidx.compose.runtime.Composable",
"import io.github.dautovicharis.charts.HistogramChart",
"import io.github.dautovicharis.charts.model.toChartDataSet",
)
const val STYLE_IMPORT = "import io.github.dautovicharis.charts.style.HistogramChartDefaults"
const val COMPONENT_NAME = "HistogramChart"
const val STYLE_BUILDER = "HistogramChartDefaults.style"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package codegen.histogram

import androidx.compose.runtime.Composable
import io.github.dautovicharis.charts.style.HistogramChartDefaults
import model.BarStyleState
import model.StylePropertiesSnapshot

@Composable
fun histogramStylePropertiesSnapshot(
styleState: BarStyleState,
seriesCount: Int,
): StylePropertiesSnapshot {
val defaultStyle = HistogramChartDefaults.style()
val normalizedBarColors =
styleState.barColors?.let { colors ->
normalizeColorCount(colors = colors, targetCount = seriesCount)
}
val currentStyle =
HistogramChartDefaults.style(
barColor = styleState.barColor ?: defaultStyle.barColor,
barColors = normalizedBarColors ?: defaultStyle.barColors,
barAlpha = styleState.barAlpha ?: defaultStyle.barAlpha,
gridVisible = styleState.gridVisible ?: defaultStyle.gridVisible,
axisVisible = styleState.axisVisible ?: defaultStyle.axisVisible,
selectionLineVisible = styleState.selectionLineVisible ?: defaultStyle.selectionLineVisible,
selectionLineWidth = styleState.selectionLineWidth ?: defaultStyle.selectionLineWidth,
zoomControlsVisible = styleState.zoomControlsVisible ?: defaultStyle.zoomControlsVisible,
)
return StylePropertiesSnapshot(
current = currentStyle.getProperties(),
defaults = defaultStyle.getProperties(),
)
}

private fun normalizeColorCount(
colors: List<androidx.compose.ui.graphics.Color>,
targetCount: Int,
): List<androidx.compose.ui.graphics.Color> {
if (targetCount <= 0 || colors.isEmpty()) return emptyList()
return List(targetCount) { index -> colors[index % colors.size] }
}
1 change: 1 addition & 0 deletions playground/src/jsMain/kotlin/model/ChartType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ enum class ChartType(
LINE("Line", "LineChart"),
MULTI_LINE("Multi Line", "MultiLineChart"),
BAR("Bar", "BarChart"),
HISTOGRAM("Histogram", "HistogramChart"),
STACKED_BAR("Stacked Bar", "StackedBarChart"),
AREA("Area", "AreaChart"),
RADAR("Radar", "RadarChart"),
Expand Down
3 changes: 3 additions & 0 deletions playground/src/jsMain/kotlin/model/PlaygroundChartRegistry.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package model

import model.definitions.AreaChartDefinition
import model.definitions.BarChartDefinition
import model.definitions.HistogramChartDefinition
import model.definitions.LineChartDefinition
import model.definitions.MultiLineChartDefinition
import model.definitions.PieChartDefinition
Expand All @@ -14,6 +15,7 @@ val playgroundChartRegistry: PlaygroundChartRegistry =
listOf(
LineChartDefinition,
BarChartDefinition,
HistogramChartDefinition,
PieChartDefinition,
RadarChartDefinition,
AreaChartDefinition,
Expand All @@ -31,6 +33,7 @@ val playgroundChartRegistry: PlaygroundChartRegistry =
overflowChartTypes =
listOf(
ChartType.MULTI_LINE,
ChartType.HISTOGRAM,
ChartType.STACKED_BAR,
),
)
10 changes: 10 additions & 0 deletions playground/src/jsMain/kotlin/model/PlaygroundModels.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const val PIE_CHART_TITLE = "Revenue Breakdown"
const val LINE_CHART_TITLE = "Monthly Trend"
const val MULTI_LINE_CHART_TITLE = "Revenue By Channel"
const val BAR_CHART_TITLE = "Weekly Performance"
const val HISTOGRAM_CHART_TITLE = "Request Duration Distribution"
const val STACKED_BAR_CHART_TITLE = "Quarterly Revenue Mix"
const val AREA_CHART_TITLE = "Plan Distribution"
const val RADAR_CHART_TITLE = "Platform Capability"
Expand Down Expand Up @@ -128,6 +129,15 @@ data class BarCodegenConfig(
val functionName: String = "PlaygroundBarChartExample",
)

data class HistogramCodegenConfig(
val points: List<BarPointInput>,
val title: String = HISTOGRAM_CHART_TITLE,
val style: BarStyleState = BarStyleState(),
val styleProperties: StylePropertiesSnapshot? = null,
val codegenMode: CodegenMode = CodegenMode.MINIMAL,
val functionName: String = "PlaygroundHistogramChartExample",
)

data class MultiLineCodegenConfig(
val series: List<MultiSeriesCodegenInput>,
val categories: List<String>,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package model.definitions

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import codegen.histogram.HistogramChartCodeGenerator
import codegen.histogram.histogramStylePropertiesSnapshot
import io.github.dautovicharis.charts.HistogramChart
import io.github.dautovicharis.charts.model.toChartDataSet
import io.github.dautovicharis.charts.style.HistogramChartDefaults
import model.BarPointInput
import model.BarStyleState
import model.ChartType
import model.DataEditorColumn
import model.DataEditorState
import model.GeneratedSnippet
import model.HISTOGRAM_CHART_TITLE
import model.HistogramCodegenConfig
import model.PlaygroundChartDefinition
import model.PlaygroundChartSession
import model.PlaygroundDataModel
import model.PlaygroundStyleState
import model.PlaygroundValidationResult
import model.SettingDescriptor
import model.deriveFunctionName
import model.formatEditorFloat
import kotlin.random.Random

internal object HistogramChartDefinition : PlaygroundChartDefinition {
private val generator = HistogramChartCodeGenerator()

override val type: ChartType = ChartType.HISTOGRAM
override val displayName: String = type.displayName
override val defaultTitle: String = HISTOGRAM_CHART_TITLE

override fun defaultDataModel(): PlaygroundDataModel =
PlaygroundSampleUseCases.histogram.initialHistogramDataSet().toSimpleSeries()

override fun defaultStyleState(): PlaygroundStyleState = BarStyleState()

override fun createEditorState(model: PlaygroundDataModel): DataEditorState {
val data = model as? PlaygroundDataModel.SimpleSeries ?: defaultDataModel() as PlaygroundDataModel.SimpleSeries
return createSimpleSeriesEditor(
model = data,
minRows = 2,
labelHeader = "Bin",
)
}

override fun validate(editorState: DataEditorState): PlaygroundValidationResult {
if (editorState.rows.size < 2) {
return PlaygroundValidationResult(
sanitizedEditor = null,
dataModel = null,
message = "Histogram chart needs at least 2 rows.",
)
}

val parsed =
parseEditorTable(
editorState = editorState,
labelPrefix = "Bin",
clampToPositive = false,
) ?: return invalidNumericResult(editorState)

val valueColumn = parsed.numericColumns.firstOrNull() ?: return invalidNumericResult(editorState)
val values = parsed.valuesByColumn.getValue(valueColumn.id)
val negativeRowIds =
values
.mapIndexedNotNull { index, value ->
if (value < 0f) editorState.rows.getOrNull(index)?.id else null
}.toSet()

if (negativeRowIds.isNotEmpty()) {
return PlaygroundValidationResult(
sanitizedEditor = null,
dataModel = null,
message = "Histogram values must be non-negative.",
invalidRowIds = negativeRowIds,
)
}

return PlaygroundValidationResult(
sanitizedEditor = editorState.copy(rows = parsed.sanitizedRows),
dataModel = PlaygroundDataModel.SimpleSeries(values = values, labels = parsed.labels),
message = "Applied ${parsed.labels.size} rows.",
)
}

override fun newRowCells(
rowIndex: Int,
columns: List<DataEditorColumn>,
): Map<String, String> = defaultRowCells(columns, rowIndex, labelPrefix = "Bin")

override fun randomize(editorState: DataEditorState): DataEditorState =
randomizeEditorValues(
editorState = editorState,
valueProvider = { Random.nextFloat() * 80f },
)

override fun settingsSchema(session: PlaygroundChartSession): List<SettingDescriptor> =
listOf(
SettingDescriptor.Section("Bars"),
SettingDescriptor.Slider(
id = "barAlpha",
label = "Bar Transparency",
defaultValue = 0.4f,
min = 0f,
max = 1f,
steps = 20,
read = { style -> (style as BarStyleState).barAlpha },
write = { style, value -> (style as BarStyleState).copy(barAlpha = value) },
),
SettingDescriptor.Color(
id = "barColor",
label = "Bar Color",
read = { style -> (style as BarStyleState).barColor },
write = { style, value -> (style as BarStyleState).copy(barColor = value) },
),
SettingDescriptor.ColorPalette(
id = "barColors",
title = "Bar Colors",
itemCount = {
val data = it.appliedData as PlaygroundDataModel.SimpleSeries
data.values.size
},
read = { style -> (style as BarStyleState).barColors },
write = { style, value -> (style as BarStyleState).copy(barColors = value) },
),
SettingDescriptor.Divider,
SettingDescriptor.Section("Visibility"),
SettingDescriptor.Toggle(
id = "gridVisible",
label = "Show Grid",
defaultValue = true,
read = { style -> (style as BarStyleState).gridVisible },
write = { style, value -> (style as BarStyleState).copy(gridVisible = value) },
),
SettingDescriptor.Toggle(
id = "axisVisible",
label = "Show Axes",
defaultValue = true,
read = { style -> (style as BarStyleState).axisVisible },
write = { style, value -> (style as BarStyleState).copy(axisVisible = value) },
),
SettingDescriptor.Toggle(
id = "selectionLineVisible",
label = "Show Selection Line",
defaultValue = true,
read = { style -> (style as BarStyleState).selectionLineVisible },
write = { style, value -> (style as BarStyleState).copy(selectionLineVisible = value) },
),
SettingDescriptor.Slider(
id = "selectionLineWidth",
label = "Selection Line Width",
defaultValue = 1f,
min = 0f,
max = 4f,
steps = 16,
read = { style -> (style as BarStyleState).selectionLineWidth },
write = { style, value -> (style as BarStyleState).copy(selectionLineWidth = value) },
),
SettingDescriptor.Toggle(
id = "zoomControlsVisible",
label = "Show Zoom Controls",
defaultValue = true,
read = { style -> (style as BarStyleState).zoomControlsVisible },
write = { style, value -> (style as BarStyleState).copy(zoomControlsVisible = value) },
),
)

@Composable
override fun renderPreview(
session: PlaygroundChartSession,
modifier: Modifier,
) {
val data = session.appliedData as PlaygroundDataModel.SimpleSeries
val styleState = session.styleState as BarStyleState
val dataSet =
data.values.toChartDataSet(
title = session.title,
labels = data.labels,
)
val defaultStyle = HistogramChartDefaults.style()
val normalizedBarColors =
styleState.barColors?.let { colors ->
normalizeColorCount(colors = colors, targetCount = data.values.size)
}
val style =
HistogramChartDefaults.style(
barColor = styleState.barColor ?: defaultStyle.barColor,
barColors = normalizedBarColors ?: defaultStyle.barColors,
barAlpha = styleState.barAlpha ?: defaultStyle.barAlpha,
gridVisible = styleState.gridVisible ?: defaultStyle.gridVisible,
axisVisible = styleState.axisVisible ?: defaultStyle.axisVisible,
selectionLineVisible = styleState.selectionLineVisible ?: defaultStyle.selectionLineVisible,
selectionLineWidth = styleState.selectionLineWidth ?: defaultStyle.selectionLineWidth,
zoomControlsVisible = styleState.zoomControlsVisible ?: defaultStyle.zoomControlsVisible,
)
HistogramChart(dataSet = dataSet, style = style)
}

@Composable
override fun generateCode(session: PlaygroundChartSession): GeneratedSnippet {
val data = session.appliedData as PlaygroundDataModel.SimpleSeries
val style = session.styleState as BarStyleState
val points =
data.values.mapIndexed { index, value ->
BarPointInput(
label = data.labels?.getOrNull(index) ?: "Bin ${index + 1}",
valueText = formatEditorFloat(value.coerceAtLeast(0f)),
)
}

return generator.generate(
HistogramCodegenConfig(
points = points,
title = session.title,
style = style,
styleProperties = histogramStylePropertiesSnapshot(style, data.values.size),
codegenMode = session.codegenMode,
functionName = deriveFunctionName(session.title, type),
),
)
}
}
Loading
Loading