Repository: cdklabs/cdk-monitoring-constructs (language: python)

Issue:
[custom] Add `width` property for `CustomMetricGroup`
### Discussed in https://github.com/cdklabs/cdk-monitoring-constructs/discussions/450

<div type='discussions-op-text'>

<sup>Originally posted by **ericxinzhang** November  2, 2023</sup>
My code looks as following. I can specify `height` in `CustomMonitoringProps`. I am wondering why `CustomMetricGroup` does not have a `width` property?

I understand that for high level APIs (e.g. `monitor<ResourceType>`) it's easier for the library to calculate width dynamically. However for `CustomMetricGroup` may we have a `width` property?


```Javascript
const monitoring = new MonitoringFacade(this, 'MyDashboard');

monitoring.monitorCustom({
  height: 5,
  humanReadableName: 'API',
  metricGroups: [
    {
        title: 'API Errors',
        // I wish I could specify `width` here
        graphWidgetLegend: cw.LegendPosition.RIGHT,
        metrics:[
          new cloudwatch.Metric({...}),
          new cloudwatch.Metric({...}),
       ],
    },
    {
        title: 'API Latency',
        graphWidgetLegend: cw.LegendPosition.RIGHT,
        metrics:[
          new cloudwatch.Metric({...}),
          new cloudwatch.Metric({...}),
       ],
    }
  ],
});
```</div>


Discussion / hints:

Fix patch:
diff --git a/API.md b/API.md
index b5d45df3..568812ba 100644
--- a/API.md
+++ b/API.md
@@ -12170,6 +12170,7 @@ const customMetricGroup: CustomMetricGroup = { ... }
 | <code><a href="#cdk-monitoring-constructs.CustomMetricGroup.property.graphWidgetRightAxis">graphWidgetRightAxis</a></code> | <code>aws-cdk-lib.aws_cloudwatch.YAxisProps</code> | optional right axis. |
 | <code><a href="#cdk-monitoring-constructs.CustomMetricGroup.property.graphWidgetSetPeriodToTimeRange">graphWidgetSetPeriodToTimeRange</a></code> | <code>boolean</code> | *No description.* |
 | <code><a href="#cdk-monitoring-constructs.CustomMetricGroup.property.graphWidgetType">graphWidgetType</a></code> | <code><a href="#cdk-monitoring-constructs.GraphWidgetType">GraphWidgetType</a></code> | type of the widget. |
+| <code><a href="#cdk-monitoring-constructs.CustomMetricGroup.property.graphWidgetWidth">graphWidgetWidth</a></code> | <code>number</code> | Width of graph widget. |
 | <code><a href="#cdk-monitoring-constructs.CustomMetricGroup.property.horizontalAnnotations">horizontalAnnotations</a></code> | <code>aws-cdk-lib.aws_cloudwatch.HorizontalAnnotation[]</code> | Optional custom horizontal annotations which will be displayed over the metrics on the left axis (if there are any alarms, any existing annotations will be merged together). |
 | <code><a href="#cdk-monitoring-constructs.CustomMetricGroup.property.horizontalRightAnnotations">horizontalRightAnnotations</a></code> | <code>aws-cdk-lib.aws_cloudwatch.HorizontalAnnotation[]</code> | Optional custom horizontal annotations which will be displayed over the metrics on the right axis (if there are any alarms, any existing annotations will be merged together). |
 | <code><a href="#cdk-monitoring-constructs.CustomMetricGroup.property.important">important</a></code> | <code>boolean</code> | *No description.* |
@@ -12278,6 +12279,22 @@ type of the widget.
 
 ---
 
+##### `graphWidgetWidth`<sup>Optional</sup> <a name="graphWidgetWidth" id="cdk-monitoring-constructs.CustomMetricGroup.property.graphWidgetWidth"></a>
+
+```typescript
+public readonly graphWidgetWidth: number;
+```
+
+- *Type:* number
+- *Default:* Automatically calculcated width, generally as wide as possible considering all metrics' widgets.
+
+Width of graph widget.
+
+Note that widgets will overflow into new rows if the summed width
+exceeds 24.
+
+---
+
 ##### `horizontalAnnotations`<sup>Optional</sup> <a name="horizontalAnnotations" id="cdk-monitoring-constructs.CustomMetricGroup.property.horizontalAnnotations"></a>
 
 ```typescript
@@ -12704,7 +12721,7 @@ const customMonitoringProps: CustomMonitoringProps = { ... }
 | <code><a href="#cdk-monitoring-constructs.CustomMonitoringProps.property.metricGroups">metricGroups</a></code> | <code><a href="#cdk-monitoring-constructs.CustomMetricGroup">CustomMetricGroup</a>[]</code> | define metric groups and metrics inside them (each metric group represents a widget). |
 | <code><a href="#cdk-monitoring-constructs.CustomMonitoringProps.property.description">description</a></code> | <code>string</code> | optional description of the whole section, in markdown. |
 | <code><a href="#cdk-monitoring-constructs.CustomMonitoringProps.property.descriptionWidgetHeight">descriptionWidgetHeight</a></code> | <code>number</code> | optional height of the description widget, so the content fits. |
-| <code><a href="#cdk-monitoring-constructs.CustomMonitoringProps.property.height">height</a></code> | <code>number</code> | height override. |
+| <code><a href="#cdk-monitoring-constructs.CustomMonitoringProps.property.height">height</a></code> | <code>number</code> | Height override. |
 
 ---
 
@@ -12853,7 +12870,7 @@ public readonly height: number;
 - *Type:* number
 - *Default:* default height
 
-height override.
+Height override.
 
 ---
 
diff --git a/lib/common/widget/size.ts b/lib/common/widget/size.ts
index fb11f0d6..074cc177 100644
--- a/lib/common/widget/size.ts
+++ b/lib/common/widget/size.ts
@@ -20,6 +20,7 @@ export const DefaultLogWidgetHeight = 7;
 /**
  * Suggests the best widget width, given the total number of widgets.
  * The main point is to make widgets as wide as possible, while saving vertical space and minimizing number of gaps.
+ *
  * @param numTotalWidgets total number of widgets to be placed
  */
 export function recommendedWidgetWidth(numTotalWidgets: number) {
diff --git a/lib/monitoring/custom/CustomMonitoring.ts b/lib/monitoring/custom/CustomMonitoring.ts
index 933758ae..908b2090 100644
--- a/lib/monitoring/custom/CustomMonitoring.ts
+++ b/lib/monitoring/custom/CustomMonitoring.ts
@@ -174,6 +174,13 @@ export interface CustomMetricGroup {
    * @see {GraphWidgetProps.setPeriodToTimeRange}
    */
   readonly graphWidgetSetPeriodToTimeRange?: boolean;
+  /**
+   * Width of graph widget. Note that widgets will overflow into new rows if the summed width
+   * exceeds 24.
+   *
+   * @default - Automatically calculcated width, generally as wide as possible considering all metrics' widgets.
+   */
+  readonly graphWidgetWidth?: number;
   /**
    * @deprecated use addToSummaryDashboard. addToSummaryDashboard will take precedence over important.
    * @see addToSummaryDashboard
@@ -207,17 +214,20 @@ export interface CustomMetricGroup {
 export interface CustomMonitoringProps extends BaseMonitoringProps {
   /**
    * optional description of the whole section, in markdown
-   * @default no description
+   *
+   * @default - no description
    */
   readonly description?: string;
   /**
    * optional height of the description widget, so the content fits
-   * @default minimum height (should fit one or two lines of text)
+   *
+   * @default - minimum height (should fit one or two lines of text)
    */
   readonly descriptionWidgetHeight?: number;
   /**
-   * height override
-   * @default default height
+   * Height override.
+   *
+   * @default - default height
    */
   readonly height?: number;
   /**
@@ -380,9 +390,6 @@ export class CustomMonitoring extends Monitoring {
     summary: boolean
   ) {
     const widgets: IWidget[] = [];
-    const metricGroupWidgetWidth = recommendedWidgetWidth(
-      annotatedGroups.length
-    );
     const metricGroupWidgetHeightDefault = summary
       ? DefaultSummaryWidgetHeight
       : DefaultGraphWidgetHeight;
@@ -416,7 +423,9 @@ export class CustomMonitoring extends Monitoring {
 
       const graphWidgetProps: GraphWidgetProps = {
         title,
-        width: metricGroupWidgetWidth,
+        width:
+          annotatedGroup.metricGroup.graphWidgetWidth ??
+          recommendedWidgetWidth(annotatedGroups.length),
         height: metricGroupWidgetHeight,
         left,
         right,


Test patch:
diff --git a/test/monitoring/custom/CustomMonitoring.test.ts b/test/monitoring/custom/CustomMonitoring.test.ts
index a246bf3f..76318bcd 100644
--- a/test/monitoring/custom/CustomMonitoring.test.ts
+++ b/test/monitoring/custom/CustomMonitoring.test.ts
@@ -39,6 +39,7 @@ test("snapshot test", () => {
       {
         title: "DummyGroup1",
         addToSummaryDashboard: true,
+        graphWidgetWidth: 6,
         metrics: [
           // regular metric
           new Metric({ metricName: "DummyMetric1", namespace, dimensionsMap }),
@@ -75,6 +76,7 @@ test("snapshot test", () => {
       {
         title: "DummyGroup2",
         graphWidgetType: GraphWidgetType.BAR,
+        graphWidgetWidth: 2,
         metrics: [
           // regular metric
           new Metric({ metricName: "DummyMetric10", namespace, dimensionsMap }),
diff --git a/test/monitoring/custom/__snapshots__/CustomMonitoring.test.ts.snap b/test/monitoring/custom/__snapshots__/CustomMonitoring.test.ts.snap
index 2f14002f..efa55fc6 100644
--- a/test/monitoring/custom/__snapshots__/CustomMonitoring.test.ts.snap
+++ b/test/monitoring/custom/__snapshots__/CustomMonitoring.test.ts.snap
@@ -652,15 +652,15 @@ Object {
               Object {
                 "Ref": "AWS::Region",
               },
-              "\\",\\"metrics\\":[[\\"DummyCustomNamespace\\",\\"DummyMetric1\\",\\"CustomDimension\\",\\"CustomDimensionValue\\"],[\\"DummyCustomNamespace\\",\\"DummyMetric2\\",\\"CustomDimension\\",\\"CustomDimensionValue\\"],[\\"DummyCustomNamespace\\",\\"DummyMetric3\\",\\"CustomDimension\\",\\"CustomDimensionValue\\"],[{\\"label\\":\\" \\",\\"expression\\":\\"SEARCH('{DummyCustomNamespace,CustomDimension} CustomDimension=\\\\\\"CustomDimensionValue\\\\\\" DummyMetric4-', 'Sum', 300)\\"}]],\\"annotations\\":{\\"horizontal\\":[{\\"label\\":\\"DummyMetric3 < 90 for 3 datapoints within 15 minutes\\",\\"value\\":90,\\"yAxis\\":\\"left\\"},{\\"label\\":\\"DummyMetric3 < 50 for 3 datapoints within 15 minutes\\",\\"value\\":50,\\"yAxis\\":\\"left\\"}]},\\"yAxis\\":{}}},{\\"type\\":\\"metric\\",\\"width\\":6,\\"height\\":100,\\"x\\":6,\\"y\\":3,\\"properties\\":{\\"view\\":\\"bar\\",\\"title\\":\\"DummyGroup2\\",\\"region\\":\\"",
+              "\\",\\"metrics\\":[[\\"DummyCustomNamespace\\",\\"DummyMetric1\\",\\"CustomDimension\\",\\"CustomDimensionValue\\"],[\\"DummyCustomNamespace\\",\\"DummyMetric2\\",\\"CustomDimension\\",\\"CustomDimensionValue\\"],[\\"DummyCustomNamespace\\",\\"DummyMetric3\\",\\"CustomDimension\\",\\"CustomDimensionValue\\"],[{\\"label\\":\\" \\",\\"expression\\":\\"SEARCH('{DummyCustomNamespace,CustomDimension} CustomDimension=\\\\\\"CustomDimensionValue\\\\\\" DummyMetric4-', 'Sum', 300)\\"}]],\\"annotations\\":{\\"horizontal\\":[{\\"label\\":\\"DummyMetric3 < 90 for 3 datapoints within 15 minutes\\",\\"value\\":90,\\"yAxis\\":\\"left\\"},{\\"label\\":\\"DummyMetric3 < 50 for 3 datapoints within 15 minutes\\",\\"value\\":50,\\"yAxis\\":\\"left\\"}]},\\"yAxis\\":{}}},{\\"type\\":\\"metric\\",\\"width\\":2,\\"height\\":100,\\"x\\":6,\\"y\\":3,\\"properties\\":{\\"view\\":\\"bar\\",\\"title\\":\\"DummyGroup2\\",\\"region\\":\\"",
               Object {
                 "Ref": "AWS::Region",
               },
-              "\\",\\"metrics\\":[[\\"DummyCustomNamespace\\",\\"DummyMetric10\\",\\"CustomDimension\\",\\"CustomDimensionValue\\"],[\\"DummyCustomNamespace\\",\\"DummyMetric11\\",\\"CustomDimension\\",\\"CustomDimensionValue\\"],[\\"DummyCustomNamespace\\",\\"DummyMetric12\\",\\"CustomDimension\\",\\"CustomDimensionValue\\"]],\\"annotations\\":{\\"horizontal\\":[{\\"label\\":\\"DummyAnnotation1\\",\\"value\\":30,\\"fill\\":\\"above\\",\\"yAxis\\":\\"left\\"},{\\"label\\":\\"DummyAnnotation2\\",\\"value\\":20,\\"fill\\":\\"below\\",\\"yAxis\\":\\"left\\"},{\\"label\\":\\"DummyMetric12 > 10 for 3 datapoints within 15 minutes\\",\\"value\\":10,\\"yAxis\\":\\"left\\"},{\\"label\\":\\"DummyMetric12 > 50 for 3 datapoints within 15 minutes\\",\\"value\\":50,\\"yAxis\\":\\"left\\"}]},\\"yAxis\\":{}}},{\\"type\\":\\"metric\\",\\"width\\":6,\\"height\\":100,\\"x\\":12,\\"y\\":3,\\"properties\\":{\\"view\\":\\"pie\\",\\"title\\":\\"DummyGroup3\\",\\"region\\":\\"",
+              "\\",\\"metrics\\":[[\\"DummyCustomNamespace\\",\\"DummyMetric10\\",\\"CustomDimension\\",\\"CustomDimensionValue\\"],[\\"DummyCustomNamespace\\",\\"DummyMetric11\\",\\"CustomDimension\\",\\"CustomDimensionValue\\"],[\\"DummyCustomNamespace\\",\\"DummyMetric12\\",\\"CustomDimension\\",\\"CustomDimensionValue\\"]],\\"annotations\\":{\\"horizontal\\":[{\\"label\\":\\"DummyAnnotation1\\",\\"value\\":30,\\"fill\\":\\"above\\",\\"yAxis\\":\\"left\\"},{\\"label\\":\\"DummyAnnotation2\\",\\"value\\":20,\\"fill\\":\\"below\\",\\"yAxis\\":\\"left\\"},{\\"label\\":\\"DummyMetric12 > 10 for 3 datapoints within 15 minutes\\",\\"value\\":10,\\"yAxis\\":\\"left\\"},{\\"label\\":\\"DummyMetric12 > 50 for 3 datapoints within 15 minutes\\",\\"value\\":50,\\"yAxis\\":\\"left\\"}]},\\"yAxis\\":{}}},{\\"type\\":\\"metric\\",\\"width\\":6,\\"height\\":100,\\"x\\":8,\\"y\\":3,\\"properties\\":{\\"view\\":\\"pie\\",\\"title\\":\\"DummyGroup3\\",\\"region\\":\\"",
               Object {
                 "Ref": "AWS::Region",
               },
-              "\\",\\"metrics\\":[[\\"DummyCustomNamespace\\",\\"DummyMetric20\\",\\"CustomDimension\\",\\"CustomDimensionValue\\"],[\\"DummyCustomNamespace\\",\\"DummyMetric21\\",\\"CustomDimension\\",\\"CustomDimensionValue\\"],[\\"DummyCustomNamespace\\",\\"DummyMetric22\\",\\"CustomDimension\\",\\"CustomDimensionValue\\",{\\"yAxis\\":\\"right\\"}]],\\"annotations\\":{\\"horizontal\\":[{\\"label\\":\\"DummyAnnotation1\\",\\"value\\":30,\\"fill\\":\\"above\\",\\"yAxis\\":\\"left\\"},{\\"label\\":\\"DummyAnnotation2\\",\\"value\\":20,\\"fill\\":\\"below\\",\\"yAxis\\":\\"left\\"},{\\"label\\":\\"DummyAnnotation3\\",\\"value\\":20,\\"fill\\":\\"below\\",\\"yAxis\\":\\"right\\"},{\\"label\\":\\"DummyMetric22 > 10 for 3 datapoints within 15 minutes\\",\\"value\\":10,\\"yAxis\\":\\"right\\"},{\\"label\\":\\"DummyMetric22 > 50 for 3 datapoints within 15 minutes\\",\\"value\\":50,\\"yAxis\\":\\"right\\"}],\\"vertical\\":[{\\"value\\":\\"2021-07-29T02:31:09.890Z\\",\\"color\\":\\"667788\\",\\"fill\\":\\"after\\",\\"label\\":\\"this is a vertical annotation\\"}]},\\"yAxis\\":{\\"right\\":{\\"label\\":\\"CustomValue\\"}},\\"legend\\":{\\"position\\":\\"right\\"},\\"setPeriodToTimeRange\\":true}},{\\"type\\":\\"metric\\",\\"width\\":6,\\"height\\":100,\\"x\\":18,\\"y\\":3,\\"properties\\":{\\"view\\":\\"singleValue\\",\\"title\\":\\"DummyGroup4\\",\\"region\\":\\"",
+              "\\",\\"metrics\\":[[\\"DummyCustomNamespace\\",\\"DummyMetric20\\",\\"CustomDimension\\",\\"CustomDimensionValue\\"],[\\"DummyCustomNamespace\\",\\"DummyMetric21\\",\\"CustomDimension\\",\\"CustomDimensionValue\\"],[\\"DummyCustomNamespace\\",\\"DummyMetric22\\",\\"CustomDimension\\",\\"CustomDimensionValue\\",{\\"yAxis\\":\\"right\\"}]],\\"annotations\\":{\\"horizontal\\":[{\\"label\\":\\"DummyAnnotation1\\",\\"value\\":30,\\"fill\\":\\"above\\",\\"yAxis\\":\\"left\\"},{\\"label\\":\\"DummyAnnotation2\\",\\"value\\":20,\\"fill\\":\\"below\\",\\"yAxis\\":\\"left\\"},{\\"label\\":\\"DummyAnnotation3\\",\\"value\\":20,\\"fill\\":\\"below\\",\\"yAxis\\":\\"right\\"},{\\"label\\":\\"DummyMetric22 > 10 for 3 datapoints within 15 minutes\\",\\"value\\":10,\\"yAxis\\":\\"right\\"},{\\"label\\":\\"DummyMetric22 > 50 for 3 datapoints within 15 minutes\\",\\"value\\":50,\\"yAxis\\":\\"right\\"}],\\"vertical\\":[{\\"value\\":\\"2021-07-29T02:31:09.890Z\\",\\"color\\":\\"667788\\",\\"fill\\":\\"after\\",\\"label\\":\\"this is a vertical annotation\\"}]},\\"yAxis\\":{\\"right\\":{\\"label\\":\\"CustomValue\\"}},\\"legend\\":{\\"position\\":\\"right\\"},\\"setPeriodToTimeRange\\":true}},{\\"type\\":\\"metric\\",\\"width\\":6,\\"height\\":100,\\"x\\":14,\\"y\\":3,\\"properties\\":{\\"view\\":\\"singleValue\\",\\"title\\":\\"DummyGroup4\\",\\"region\\":\\"",
               Object {
                 "Ref": "AWS::Region",
               },
@@ -903,7 +903,7 @@ Min number of samples to alarm: 5",
           "Fn::Join": Array [
             "",
             Array [
-              "{\\"widgets\\":[{\\"type\\":\\"text\\",\\"width\\":24,\\"height\\":1,\\"x\\":0,\\"y\\":0,\\"properties\\":{\\"markdown\\":\\"### DummyName\\"}},{\\"type\\":\\"metric\\",\\"width\\":24,\\"height\\":100,\\"x\\":0,\\"y\\":1,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"DummyGroup1\\",\\"region\\":\\"",
+              "{\\"widgets\\":[{\\"type\\":\\"text\\",\\"width\\":24,\\"height\\":1,\\"x\\":0,\\"y\\":0,\\"properties\\":{\\"markdown\\":\\"### DummyName\\"}},{\\"type\\":\\"metric\\",\\"width\\":6,\\"height\\":100,\\"x\\":0,\\"y\\":1,\\"properties\\":{\\"view\\":\\"timeSeries\\",\\"title\\":\\"DummyGroup1\\",\\"region\\":\\"",
               Object {
                 "Ref": "AWS::Region",
               },