From e1dcc108b0fecbe4eee2886a15e9332baf4168e2 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Fri, 26 Jun 2026 16:49:44 -0700 Subject: [PATCH] feat(model): add data_visualization layout block Add support for the Block Kit `data_visualization` layout block to slack-api-model. A data visualization renders a pie, bar, area, or line chart from supplied data. - Add DataVisualizationBlock model (title, chart, block_id) following existing LayoutBlock conventions, plus nested DataVisualizationChart, DataVisualizationSegment (pie), DataVisualizationSeries and DataVisualizationDataPoint (bar/area/line), and DataVisualizationAxisConfig. - Register data_visualization in GsonLayoutBlockFactory for deserialization. - Add a Blocks.dataVisualization(...) DSL helper. - Add BlockKitTest cases covering pie/bar JSON parsing and builder round-trip. Ref: https://docs.slack.dev/reference/block-kit/blocks/data-visualization-block Co-Authored-By: Claude --- .../com/slack/api/model/block/Blocks.java | 6 + .../block/DataVisualizationAxisConfig.java | 34 ++++++ .../model/block/DataVisualizationBlock.java | 33 ++++++ .../model/block/DataVisualizationChart.java | 43 ++++++++ .../block/DataVisualizationDataPoint.java | 28 +++++ .../model/block/DataVisualizationSegment.java | 27 +++++ .../model/block/DataVisualizationSeries.java | 29 +++++ .../api/util/json/GsonLayoutBlockFactory.java | 2 + .../api/model/block/BlockKitTest.java | 104 ++++++++++++++++++ 9 files changed, 306 insertions(+) create mode 100644 slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationAxisConfig.java create mode 100644 slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationBlock.java create mode 100644 slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationChart.java create mode 100644 slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationDataPoint.java create mode 100644 slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationSegment.java create mode 100644 slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationSeries.java diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java b/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java index fd451d8d2..6a7c9bb51 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java +++ b/slack-api-model/src/main/java/com/slack/api/model/block/Blocks.java @@ -129,4 +129,10 @@ public static ShareShortcutBlock shareShortcut() { return ShareShortcutBlock.builder().build(); } + // DataVisualizationBlock + + public static DataVisualizationBlock dataVisualization(ModelConfigurator configurator) { + return configurator.configure(DataVisualizationBlock.builder()).build(); + } + } diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationAxisConfig.java b/slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationAxisConfig.java new file mode 100644 index 000000000..7a4ca2757 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationAxisConfig.java @@ -0,0 +1,34 @@ +package com.slack.api.model.block; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * The axis configuration of a bar, area, or line {@link DataVisualizationChart}. + * + * @see Data visualization block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DataVisualizationAxisConfig { + /** + * The x-axis labels that define the display order of categories. Each maximum 20 characters. + */ + private List categories; + + /** + * The x-axis title. Maximum 50 characters. + */ + private String xLabel; + + /** + * The y-axis title. Maximum 50 characters. + */ + private String yLabel; +} diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationBlock.java b/slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationBlock.java new file mode 100644 index 000000000..14e2205bb --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationBlock.java @@ -0,0 +1,33 @@ +package com.slack.api.model.block; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * A data visualization is a layout block used to render a chart from supplied data. The chart may be + * a pie, bar, area, or line chart, configured via the {@link DataVisualizationChart chart} object. + * + * @see Data visualization block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DataVisualizationBlock implements LayoutBlock { + public static final String TYPE = "data_visualization"; + private final String type = TYPE; + + /** + * The label displayed above the chart. Maximum 50 characters. + */ + private String title; + + /** + * The chart to render. One of pie, bar, area, or line. + */ + private DataVisualizationChart chart; + + private String blockId; +} diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationChart.java b/slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationChart.java new file mode 100644 index 000000000..701d6be47 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationChart.java @@ -0,0 +1,43 @@ +package com.slack.api.model.block; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * The chart rendered by a {@link DataVisualizationBlock}. + * + *

For a pie chart ({@code type} = {@code pie}), supply {@code segments}. For a bar, area, or line + * chart ({@code type} = {@code bar}, {@code area}, or {@code line}), supply {@code series} together + * with an {@link DataVisualizationAxisConfig axis config}.

+ * + * @see Data visualization block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DataVisualizationChart { + /** + * The type of chart. One of {@code pie}, {@code bar}, {@code area}, or {@code line}. + */ + private String type; + + /** + * The segments of a pie chart. Required for pie charts; between 1 and 6 items. + */ + private List segments; + + /** + * The data series of a bar, area, or line chart. Required for those chart types; between 1 and 6 items. + */ + private List series; + + /** + * The axis configuration for a bar, area, or line chart. Required for those chart types. + */ + private DataVisualizationAxisConfig axisConfig; +} diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationDataPoint.java b/slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationDataPoint.java new file mode 100644 index 000000000..e57ef02c5 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationDataPoint.java @@ -0,0 +1,28 @@ +package com.slack.api.model.block; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * A single data point of a {@link DataVisualizationSeries}. + * + * @see Data visualization block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DataVisualizationDataPoint { + /** + * The x-axis category. Must match a category defined in the chart's + * {@link DataVisualizationAxisConfig axis config}. Maximum 20 characters. + */ + private String label; + + /** + * The y-axis value. Negative values are permitted. + */ + private Double value; +} diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationSegment.java b/slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationSegment.java new file mode 100644 index 000000000..f523afa45 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationSegment.java @@ -0,0 +1,27 @@ +package com.slack.api.model.block; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * A single segment of a pie chart within a {@link DataVisualizationChart}. + * + * @see Data visualization block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DataVisualizationSegment { + /** + * The legend/hover text for the segment. Maximum 20 characters. + */ + private String label; + + /** + * The weight of the segment. Must be greater than 0. + */ + private Double value; +} diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationSeries.java b/slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationSeries.java new file mode 100644 index 000000000..72b80a00c --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/block/DataVisualizationSeries.java @@ -0,0 +1,29 @@ +package com.slack.api.model.block; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * A single data series of a bar, area, or line chart within a {@link DataVisualizationChart}. + * + * @see Data visualization block + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DataVisualizationSeries { + /** + * The legend identifier for the series. Must be unique per chart. Maximum 20 characters. + */ + private String name; + + /** + * The data points of the series, one per category. Between 1 and 20 items. + */ + private List data; +} diff --git a/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java b/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java index 9286e6b51..a72f8690c 100644 --- a/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java +++ b/slack-api-model/src/main/java/com/slack/api/util/json/GsonLayoutBlockFactory.java @@ -61,6 +61,8 @@ private Class getLayoutClassInstance(String typeName) { return RichTextBlock.class; case ShareShortcutBlock.TYPE: return ShareShortcutBlock.class; + case DataVisualizationBlock.TYPE: + return DataVisualizationBlock.class; default: if (failOnUnknownProperties) { throw new JsonParseException("Unsupported layout block type: " + typeName); diff --git a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java index 1fc12d58d..c274b7a26 100644 --- a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java +++ b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java @@ -1802,6 +1802,110 @@ public void parseLinkTriggerMessages() { } + @Test + public void parseDataVisualizationPie() { + // https://docs.slack.dev/reference/block-kit/blocks/data-visualization-block + String json = "{\n" + + " \"blocks\": [\n" + + " {\n" + + " \"type\": \"data_visualization\",\n" + + " \"block_id\": \"dv1\",\n" + + " \"title\": \"Sales by region\",\n" + + " \"chart\": {\n" + + " \"type\": \"pie\",\n" + + " \"segments\": [\n" + + " { \"label\": \"North\", \"value\": 40 },\n" + + " { \"label\": \"South\", \"value\": 60 }\n" + + " ]\n" + + " }\n" + + " }\n" + + " ]\n" + + "}"; + Message message = GsonFactory.createSnakeCase().fromJson(json, Message.class); + assertThat(message, is(notNullValue())); + assertThat(message.getBlocks().size(), is(1)); + DataVisualizationBlock block = (DataVisualizationBlock) message.getBlocks().get(0); + assertThat(block.getType(), is("data_visualization")); + assertThat(block.getBlockId(), is("dv1")); + assertThat(block.getTitle(), is("Sales by region")); + assertThat(block.getChart().getType(), is("pie")); + assertThat(block.getChart().getSegments().size(), is(2)); + assertThat(block.getChart().getSegments().get(0).getLabel(), is("North")); + assertThat(block.getChart().getSegments().get(0).getValue(), is(40.0)); + assertThat(block.getChart().getSeries(), is(nullValue())); + assertThat(block.getChart().getAxisConfig(), is(nullValue())); + } + + @Test + public void parseDataVisualizationBar() { + // https://docs.slack.dev/reference/block-kit/blocks/data-visualization-block + String json = "{\n" + + " \"blocks\": [\n" + + " {\n" + + " \"type\": \"data_visualization\",\n" + + " \"title\": \"Revenue\",\n" + + " \"chart\": {\n" + + " \"type\": \"bar\",\n" + + " \"series\": [\n" + + " {\n" + + " \"name\": \"2025\",\n" + + " \"data\": [\n" + + " { \"label\": \"Q1\", \"value\": 100 },\n" + + " { \"label\": \"Q2\", \"value\": -20 }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"axis_config\": {\n" + + " \"categories\": [\"Q1\", \"Q2\"],\n" + + " \"x_label\": \"Quarter\",\n" + + " \"y_label\": \"USD\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + "}"; + Message message = GsonFactory.createSnakeCase().fromJson(json, Message.class); + assertThat(message, is(notNullValue())); + assertThat(message.getBlocks().size(), is(1)); + DataVisualizationBlock block = (DataVisualizationBlock) message.getBlocks().get(0); + assertThat(block.getTitle(), is("Revenue")); + assertThat(block.getChart().getType(), is("bar")); + assertThat(block.getChart().getSeries().size(), is(1)); + assertThat(block.getChart().getSeries().get(0).getName(), is("2025")); + assertThat(block.getChart().getSeries().get(0).getData().size(), is(2)); + assertThat(block.getChart().getSeries().get(0).getData().get(1).getValue(), is(-20.0)); + assertThat(block.getChart().getAxisConfig().getCategories(), is(Arrays.asList("Q1", "Q2"))); + assertThat(block.getChart().getAxisConfig().getXLabel(), is("Quarter")); + assertThat(block.getChart().getAxisConfig().getYLabel(), is("USD")); + assertThat(block.getChart().getSegments(), is(nullValue())); + } + + @Test + public void dataVisualizationBuilderRoundTrip() { + DataVisualizationBlock block = dataVisualization(dv -> dv + .blockId("dv-builder") + .title("Sales by region") + .chart(DataVisualizationChart.builder() + .type("pie") + .segments(Arrays.asList( + DataVisualizationSegment.builder().label("North").value(40.0).build(), + DataVisualizationSegment.builder().label("South").value(60.0).build() + )) + .build())); + assertThat(block, is(notNullValue())); + assertThat(block.getType(), is("data_visualization")); + + Gson gson = GsonFactory.createSnakeCase(); + String output = gson.toJson(block); + DataVisualizationBlock parsed = gson.fromJson(output, DataVisualizationBlock.class); + assertThat(parsed.getTitle(), is("Sales by region")); + assertThat(parsed.getBlockId(), is("dv-builder")); + assertThat(parsed.getChart().getType(), is("pie")); + assertThat(parsed.getChart().getSegments().size(), is(2)); + assertThat(parsed.getChart().getSegments().get(1).getLabel(), is("South")); + assertThat(parsed.getChart().getSegments().get(1).getValue(), is(60.0)); + } + @Test public void parseRichTextElements() { String json = "{\n" +