Repository: Automattic/jetpack
Pull request #42103: Newsletter Dashboard Widget:  add chart for subscriber stats
Author: login: spsiddarthan
Labels: name: Enhancement
description: Changes to an existing feature — removing, adding, or changing parts of it
name: [Feature] Subscriptions
description: All subscription-related things such as paid and unpaid, user management, and newsletter settings.
name: [Plugin] Jetpack
description: Issues about the Jetpack plugin. https://wordpress.org/plugins/jetpack/
name: [Tests] Includes Tests
Created: 2025-02-27T11:37:21Z | Merged: 2025-03-07T00:37:50Z

<!--- Provide a general summary of your changes in the Title above -->
<!-- Would you like this feature to be tested by Beta testers?
Please add testing instructions to projects/plugins/jetpack/to-test.md in a new commit as part of your PR. -->
<!-- a12s: If you have an expected version that you're aiming for the PR to add, please use the Milestone field to communicate it. If you leave it blank, that indicates there isn't a preference. -->

Related to https://github.com/Automattic/loop/issues/450

## Proposed changes:
<!--- Explain what functional changes your PR includes -->
* Adds the chart section to the newsletter widget that shows the cumulative count of `all`, and `paid` subscribers over a rolling 60-day period.

### Other information:

- [x] Have you written new tests for your changes, if applicable?
- [x] Have you checked the E2E test CI results, and verified that your changes do not break them?
- [x] Have you tested your changes on WordPress.com, if applicable (if so, you'll see a generated comment below with a script to run)?

## Jetpack product discussion
<!-- If you're an Automattician, include a shortlink to the p2 discussion with Jetpack Product here. -->
<!-- Make sure any changes to existing products have been discussed and agreed upon -->

See linked issue.

## Does this pull request change what data or activity we track or use?
<!--- If so, please add the "[Status] Needs Privacy Updates" label and explain what changes there are. -->
<!--- Check existing Jetpack support documents for a preview of the information we need. -->

No

## Testing instructions:
<!-- If you were reviewing this PR, how would you like the instructions to be presented? -->
<!-- Please include detailed testing steps, explaining how to test your change. -->
<!-- Bear in mind that context you working on is not obvious for everyone.  -->
<!-- Adding "simple" configuration steps will help reviewers to get to your PR as quickly as possible. -->
<!-- "Before / After" screenshots can also be very helpful when the change is visual. -->

* Configure your WP environment to show the newsletter widget by adding `define( 'JETPACK_NEWSLETTER_WIDGET', true );` to `0-sandbox.php` on your sandbox.
* Make sure your sandbox is up to date because this PR requires Automattic/wpcom#176009 to work.
* Sandbox your site and `public-api`
* Navigate to your site's wp-admin
* The newsletter widget should render with a chart
* If it's not rendering, make sure you have some subscribers (email, reader, or paid) for your site
* Test with different amounts of these types of subscribers
* Check to make sure the chart looks good and different screen sizes
* Check that the data is what you expect




Diff:
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f6e9725d5868..d79c0c91dc69 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4230,6 +4230,24 @@ importers:
       '@microsoft/fetch-event-source':
         specifier: 2.0.1
         version: 2.0.1
+      '@react-spring/web':
+        specifier: 9.7.5
+        version: 9.7.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@visx/curve':
+        specifier: 3.12.0
+        version: 3.12.0
+      '@visx/legend':
+        specifier: ^3.12.0
+        version: 3.12.0(react@18.3.1)
+      '@visx/responsive':
+        specifier: 3.12.0
+        version: 3.12.0(react@18.3.1)
+      '@visx/scale':
+        specifier: ^3.12.0
+        version: 3.12.0
+      '@visx/xychart':
+        specifier: 3.12.0
+        version: 3.12.0(@react-spring/web@9.7.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       '@wordpress/base-styles':
         specifier: 5.19.0
         version: 5.19.0
@@ -6860,6 +6878,12 @@ packages:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
 
+  '@react-spring/web@9.7.5':
+    resolution: {integrity: sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+
   '@redux-saga/core@1.3.0':
     resolution: {integrity: sha512-L+i+qIGuyWn7CIg7k1MteHGfttKPmxwZR5E7OsGikCL2LzYA0RERlaUY00Y3P3ZV2EYgrsYlBrGs6cJP5OKKqA==}
 
@@ -17545,6 +17569,15 @@ snapshots:
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
 
+  '@react-spring/web@9.7.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+    dependencies:
+      '@react-spring/animated': 9.7.5(react@18.3.1)
+      '@react-spring/core': 9.7.5(react@18.3.1)
+      '@react-spring/shared': 9.7.5(react@18.3.1)
+      '@react-spring/types': 9.7.5
+      react: 18.3.1
+      react-dom: 18.3.1(react@18.3.1)
+
   '@redux-saga/core@1.3.0':
     dependencies:
       '@babel/runtime': 7.26.0
@@ -18991,6 +19024,18 @@ snapshots:
       prop-types: 15.8.1
       react: 18.3.1
 
+  '@visx/react-spring@3.12.0(@react-spring/web@9.7.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
+    dependencies:
+      '@react-spring/web': 9.7.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@types/react': 18.3.18
+      '@visx/axis': 3.12.0(react@18.3.1)
+      '@visx/grid': 3.12.0(react@18.3.1)
+      '@visx/scale': 3.12.0
+      '@visx/text': 3.12.0(react@18.3.1)
+      classnames: 2.5.1
+      prop-types: 15.8.1
+      react: 18.3.1
+
   '@visx/responsive@3.12.0(react@18.3.1)':
     dependencies:
       '@types/lodash': 4.17.16
@@ -19098,6 +19143,34 @@ snapshots:
     transitivePeerDependencies:
       - react-dom
 
+  '@visx/xychart@3.12.0(@react-spring/web@9.7.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+    dependencies:
+      '@react-spring/web': 9.7.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@types/lodash': 4.17.16
+      '@types/react': 18.3.18
+      '@visx/annotation': 3.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@visx/axis': 3.12.0(react@18.3.1)
+      '@visx/event': 3.12.0
+      '@visx/glyph': 3.12.0(react@18.3.1)
+      '@visx/grid': 3.12.0(react@18.3.1)
+      '@visx/react-spring': 3.12.0(@react-spring/web@9.7.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
+      '@visx/responsive': 3.12.0(react@18.3.1)
+      '@visx/scale': 3.12.0
+      '@visx/shape': 3.12.0(react@18.3.1)
+      '@visx/text': 3.12.0(react@18.3.1)
+      '@visx/tooltip': 3.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@visx/vendor': 3.12.0
+      '@visx/voronoi': 3.12.0(react@18.3.1)
+      classnames: 2.5.1
+      d3-interpolate-path: 2.2.1
+      d3-shape: 2.1.0
+      lodash: 4.17.21
+      mitt: 2.1.0
+      prop-types: 15.8.1
+      react: 18.3.1
+    transitivePeerDependencies:
+      - react-dom
+
   '@vitest/expect@2.0.5':
     dependencies:
       '@vitest/spy': 2.0.5
diff --git a/projects/plugins/jetpack/changelog/add-line-chart-for-subscriber-stats b/projects/plugins/jetpack/changelog/add-line-chart-for-subscriber-stats
new file mode 100644
index 000000000000..dbedf1d7cb55
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/add-line-chart-for-subscriber-stats
@@ -0,0 +1,4 @@
+Significance: minor
+Type: other
+
+Tweaked subscribers counts endpoint to return aggregated data based on a query param.
diff --git a/projects/plugins/jetpack/class-jetpack-newsletter-dashboard-widget.php b/projects/plugins/jetpack/class-jetpack-newsletter-dashboard-widget.php
index a9af429177d8..3b296690128b 100644
--- a/projects/plugins/jetpack/class-jetpack-newsletter-dashboard-widget.php
+++ b/projects/plugins/jetpack/class-jetpack-newsletter-dashboard-widget.php
@@ -54,7 +54,7 @@ public static function get_config_data() {
 
 		if ( Jetpack::is_connection_ready() ) {
 			$site_id  = Jetpack_Options::get_option( 'id' );
-			$api_path = sprintf( '/sites/%d/subscribers/counts', $site_id );
+			$api_path = sprintf( '/sites/%d/subscribers/stats', $site_id );
 			$response = Client::wpcom_json_api_request_as_blog(
 				$api_path,
 				'2',
@@ -72,6 +72,14 @@ public static function get_config_data() {
 				if ( isset( $subscriber_counts['counts']['paid_subscribers'] ) ) {
 					$config_data['paidSubscribers'] = (int) $subscriber_counts['counts']['paid_subscribers'];
 				}
+
+				if ( isset( $subscriber_counts['counts']['all_subscribers'] ) ) {
+					$config_data['allSubscribers'] = (int) $subscriber_counts['counts']['all_subscribers'];
+				}
+
+				if ( isset( $subscriber_counts['aggregate'] ) ) {
+					$config_data['subscriberTotalsByDate'] = $subscriber_counts['aggregate'];
+				}
 			}
 		}
 
diff --git a/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/components/newsletter-widget.tsx b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/components/newsletter-widget.tsx
new file mode 100644
index 000000000000..218b9f222fdc
--- /dev/null
+++ b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/components/newsletter-widget.tsx
@@ -0,0 +1,160 @@
+import { getRedirectUrl } from '@automattic/jetpack-components';
+import '../style.scss';
+import { Icon } from '@wordpress/components';
+import { createInterpolateElement } from '@wordpress/element';
+import { __, sprintf } from '@wordpress/i18n';
+import { envelope, payment } from '@wordpress/icons';
+import { buildJPRedirectSource, formatNumber, getSubscriberStatsUrl } from '../helpers';
+import { SubscribersChart } from './subscribers-chart';
+import type { DailySubscriptionStats } from '../types';
+
+export interface NewsletterWidgetProps {
+	site: string;
+	adminUrl: string;
+	isWpcomSite: boolean;
+	emailSubscribers?: number;
+	paidSubscribers?: number;
+	allSubscribers?: number;
+	subscriberTotalsByDate?: DailySubscriptionStats;
+}
+
+export const NewsletterWidget = ( {
+	site,
+	adminUrl,
+	isWpcomSite,
+	emailSubscribers = 0,
+	paidSubscribers = 0,
+	allSubscribers = 0,
+	subscriberTotalsByDate = {},
+}: NewsletterWidgetProps ) => {
+	const showStats = allSubscribers > 0 || paidSubscribers > 0;
+	const showChart = Object.values( subscriberTotalsByDate ).some(
+		day => day?.all >= 5 || day?.paid > 0
+	);
+
+	return (
+		<div className="newsletter-widget">
+			<div className="newsletter-widget__header">
+				{ showStats && (
+					<div className="newsletter-widget__stats">
+						<span className="newsletter-widget__stat-item">
+							<span className="newsletter-widget__icon">
+								<Icon icon={ envelope } size={ 24 } />
+							</span>
+							<span className="newsletter-widget__stat-content">
+								<span className="newsletter-widget__stat-label">
+									<a href={ getSubscriberStatsUrl( site, isWpcomSite, adminUrl ) }>
+										{ sprintf(
+											//translators: %1$s is the total number of subscribers, %2$s is the number of email subscribers
+											__( '%1$s subscribers (%2$s via email)', 'jetpack' ),
+											formatNumber( allSubscribers ),
+											formatNumber( emailSubscribers )
+										) }
+									</a>
+								</span>
+							</span>
+						</span>
+						<span className="newsletter-widget__stat-item">
+							<span className="newsletter-widget__icon">
+								<Icon icon={ payment } size={ 24 } />
+							</span>
+							<span className="newsletter-widget__stat-content">
+								<span className="newsletter-widget__stat-label">
+									<a href={ getSubscriberStatsUrl( site, isWpcomSite, adminUrl ) }>
+										{ sprintf(
+											//translators: %s is the number of paid subscribers
+											__( '%s paid subscriptions', 'jetpack' ),
+											formatNumber( paidSubscribers )
+										) }
+									</a>
+								</span>
+							</span>
+						</span>
+					</div>
+				) }
+			</div>
+			{ showChart && (
+				<div className="newsletter-widget__chart">
+					<h3 className="newsletter-widget__heading">{ __( 'Total Subscribers', 'jetpack' ) }</h3>
+					<SubscribersChart subscriberTotalsByDate={ subscriberTotalsByDate } />
+				</div>
+			) }
+			<div className="newsletter-widget__footer">
+				<p className="newsletter-widget__footer-msg">
+					{ createInterpolateElement(
+						__(
+							'Effortlessly turn posts into emails with our Newsletter feature-expand your reach, engage readers, and monetize your writing. No coding required. <link>Learn more</link>',
+							'jetpack'
+						),
+						{
+							link: (
+								<a
+									href={ getRedirectUrl(
+										buildJPRedirectSource(
+											'learn/courses/newsletters-101/wordpress-com-newsletter'
+										)
+									) }
+								/>
+							),
+						}
+					) }
+				</p>
+				<div>
+					<h3 className="newsletter-widget__heading">{ __( 'Quick Links', 'jetpack' ) }</h3>
+					<ul className="newsletter-widget__footer-list">
+						<li>
+							<a href={ `${ adminUrl }edit.php` }>{ __( 'Publish your next post', 'jetpack' ) }</a>
+						</li>
+						<li>
+							<a href={ getSubscriberStatsUrl( site, isWpcomSite, adminUrl ) }>
+								{ __( 'View subscriber stats', 'jetpack' ) }
+							</a>
+						</li>
+						<li>
+							<a
+								href={ getRedirectUrl(
+									buildJPRedirectSource( `subscribers/${ site }`, isWpcomSite ),
+									{ anchor: 'add-subscribers' }
+								) }
+							>
+								{ __( 'Import subscribers', 'jetpack' ) }
+							</a>
+						</li>
+						<li>
+							<a
+								href={ getRedirectUrl(
+									buildJPRedirectSource( `subscribers/${ site }`, isWpcomSite )
+								) }
+							>
+								{ __( 'Manage subscribers', 'jetpack' ) }
+							</a>
+						</li>
+						<li>
+							<a
+								href={ getRedirectUrl(
+									buildJPRedirectSource(
+										`${ isWpcomSite ? 'earn' : 'monetize' }/${ site }`,
+										isWpcomSite
+									)
+								) }
+							>
+								{ __( 'Monetize', 'jetpack' ) }
+							</a>
+						</li>
+						<li>
+							<a
+								href={
+									isWpcomSite
+										? getRedirectUrl( buildJPRedirectSource( `settings/newsletter/${ site }` ) )
+										: `${ adminUrl }admin.php?page=jetpack#newsletter`
+								}
+							>
+								{ __( 'Newsletter settings', 'jetpack' ) }
+							</a>
+						</li>
+					</ul>
+				</div>
+			</div>
+		</div>
+	);
+};
diff --git a/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/components/subscribers-chart.tsx b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/components/subscribers-chart.tsx
new file mode 100644
index 000000000000..bed35b511cbd
--- /dev/null
+++ b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/components/subscribers-chart.tsx
@@ -0,0 +1,199 @@
+import { curveMonotoneX } from '@visx/curve';
+import { LegendOrdinal } from '@visx/legend';
+import { ParentSize } from '@visx/responsive';
+import { scaleOrdinal } from '@visx/scale';
+import { Axis, Grid, LineSeries, Tooltip, XYChart, buildChartTheme } from '@visx/xychart';
+import { __, sprintf } from '@wordpress/i18n';
+import { SERIES_COLORS, SERIES_LABELS } from '../constants';
+import {
+	calcLeftAxisMargin,
+	formatAxisTickDate,
+	formatDate,
+	formatNumber,
+	getXAxisTickValues,
+	transformData,
+} from '../helpers';
+import type { DailySubscriptionStats, ChartSubscriptionDataPoint } from '../types';
+import type {
+	RenderTooltipGlyphProps,
+	RenderTooltipParams,
+} from '@visx/xychart/lib/components/Tooltip';
+
+// Create a scale for the legend
+const legendScale = scaleOrdinal( {
+	domain: [ 'all', 'paid' ],
+	range: [ SERIES_COLORS.all, SERIES_COLORS.paid ],
+} );
+
+const chartTheme = buildChartTheme( {
+	backgroundColor: 'white',
+	colors: [ SERIES_COLORS.all, SERIES_COLORS.paid ],
+	gridColor: '#e0e0e0',
+	gridColorDark: '#e0e0e0',
+	tickLength: 0,
+	gridStyles: {
+		strokeWidth: 1,
+	},
+	svgLabelSmall: {
+		fill: '#1e1e1e',
+		fontSize: 13,
+		fontWeight: 400,
+	},
+} );
+
+// Chart accessors
+const getDate = ( d: ChartSubscriptionDataPoint ) => d.date;
+const getAllSubscribers = ( d: ChartSubscriptionDataPoint ) => d.all;
+const getPaidSubscribers = ( d: ChartSubscriptionDataPoint ) => d.paid;
+const getLineColor = ( k: string ) => SERIES_COLORS[ k ];
+const getLegendLabel = ( k: string ) => SERIES_LABELS[ k ];
+
+// Custom rendering for tooltip glyphs to match the line colors
+const renderGlyph = ( {
+	key,
+	color,
+	x,
+	y,
+}: RenderTooltipGlyphProps< ChartSubscriptionDataPoint > ) => {
+	const fillColor = SERIES_COLORS[ key ] || color;
+
+	return (
+		<circle
+			key={ `glyph-${ key }` }
+			cx={ x }
+			cy={ y }
+			r={ 4 }
+			fill={ fillColor }
+			stroke="white"
+			strokeWidth={ 2 }
+		/>
+	);
+};
+
+const renderTooltip = ( { tooltipData }: RenderTooltipParams< ChartSubscriptionDataPoint > ) => {
+	if ( ! tooltipData?.nearestDatum ) return null;
+
+	const datum = tooltipData.nearestDatum.datum;
+	const date = getDate( datum );
+
+	return (
+		<>
+			<div className="subscribers-chart__tooltip-date">{ formatDate( date, 'full' ) }</div>
+			<div className="subscribers-chart__tooltip-stats">
+				<div className="subscribers-chart__tooltip-stat">
+					<div
+						style={ { backgroundColor: SERIES_COLORS.all } }
+						className="subscribers-chart__tooltip-indicator"
+					/>
+
+					<span>
+						{ sprintf(
+							// translators: %s is the total number of subscribers.
+							__( 'All: %s', 'jetpack' ),
+							formatNumber( getAllSubscribers( datum ) )
+						) }
+					</span>
+				</div>
+				<div className="subscribers-chart__tooltip-stat">
+					<div
+						style={ { backgroundColor: SERIES_COLORS.paid } }
+						className="subscribers-chart__tooltip-indicator"
+					/>
+					<span>
+						{ sprintf(
+							// translators: %s is the number of paid subscribers.
+							__( 'Paid: %s', 'jetpack' ),
+							formatNumber( getPaidSubscribers( datum ) )
+						) }
+					</span>
+				</div>
+			</div>
+		</>
+	);
+};
+
+interface SubscribersChartProps {
+	subscriberTotalsByDate: DailySubscriptionStats;
+}
+
+export const SubscribersChart = ( { subscriberTotalsByDate }: SubscribersChartProps ) => {
+	if ( Object.keys( subscriberTotalsByDate ).length === 0 ) {
+		return <div>{ __( 'No data available', 'jetpack' ) }</div>;
+	}
+
+	const data = transformData( subscriberTotalsByDate );
+
+	return (
+		<>
+			<div className="subscribers-chart">
+				<ParentSize>
+					{ ( { width, height } ) => {
+						if ( ! width || ! height ) return null;
+
+						return (
+							<XYChart
+								height={ height }
+								width={ width }
+								xScale={ { type: 'time' } }
+								yScale={ { type: 'linear', nice: true } }
+								theme={ chartTheme }
+								margin={ { top: 10, right: 30, bottom: 30, left: calcLeftAxisMargin( data ) } }
+							>
+								<Grid columns={ false } numTicks={ 5 } />
+
+								<LineSeries
+									dataKey="all"
+									data={ data }
+									xAccessor={ getDate }
+									yAccessor={ getAllSubscribers }
+									colorAccessor={ getLineColor }
+									strokeWidth={ 2 }
+									curve={ curveMonotoneX }
+								/>
+
+								<LineSeries
+									dataKey="paid"
+									data={ data }
+									xAccessor={ getDate }
+									yAccessor={ getPaidSubscribers }
+									colorAccessor={ getLineColor }
+									strokeWidth={ 2 }
+									curve={ curveMonotoneX }
+								/>
+
+								<Axis orientation="left" hideAxisLine hideTicks hideZero numTicks={ 5 } />
+
+								<Axis
+									orientation="bottom"
+									tickFormat={ formatAxisTickDate }
+									hideAxisLine
+									hideTicks
+									numTicks={ 5 }
+									tickValues={ getXAxisTickValues( data ) }
+								/>
+
+								<Tooltip< ChartSubscriptionDataPoint >
+									showVerticalCrosshair
+									showSeriesGlyphs
+									className="subscribers-chart__tooltip"
+									renderTooltip={ renderTooltip }
+									renderGlyph={ renderGlyph }
+								/>
+							</XYChart>
+						);
+					} }
+				</ParentSize>
+			</div>
+			<LegendOrdinal
+				scale={ legendScale }
+				direction="row"
+				labelFormat={ getLegendLabel }
+				shape="circle"
+				shapeWidth={ 10 }
+				shapeHeight={ 10 }
+				itemMargin={ 5 }
+				className="subscribers-chart__legend"
+			/>
+		</>
+	);
+};
diff --git a/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/constants.ts b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/constants.ts
new file mode 100644
index 000000000000..ffe1fcee563a
--- /dev/null
+++ b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/constants.ts
@@ -0,0 +1,11 @@
+import { __ } from '@wordpress/i18n';
+
+export const SERIES_COLORS = {
+	all: '#3057dc',
+	paid: '#e68b28',
+};
+
+export const SERIES_LABELS = {
+	all: __( 'All', 'jetpack' ),
+	paid: __( 'Paid', 'jetpack' ),
+};
diff --git a/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/helpers.ts b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/helpers.ts
new file mode 100644
index 000000000000..38881b4935cc
--- /dev/null
+++ b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/helpers.ts
@@ -0,0 +1,143 @@
+import { getRedirectUrl } from '@automattic/jetpack-components';
+import { dateI18n } from '@wordpress/date';
+import { SubscriberTotalsByDate, ChartSubscriptionDataPoint } from './types';
+
+/**
+ * Helper function to build the Jetpack redirect source URL.
+ * @param url         - The url to redirect to. Note: it can only be to a whitelisted domain, and query params and anchors must be passed to getRedirectUrl as arguments.
+ * @param isWpcomSite - The the site on the WordPress.com platform. Simple or WoA.
+ * @return The URL that can be passed to the getRedirectUrl function.
+ * @example
+ * const site = 'example.wordpress.com';
+ * const redirectUrl = buildJPRedirectSource( `subscribers/${ site }`, true );
+ *
+ * <a href={ getRedirectUrl( redirectUrl ) }>Subscriber</a>;
+ */
+export const buildJPRedirectSource = ( url: string, isWpcomSite: boolean = true ) => {
+	const host = isWpcomSite ? 'wordpress.com' : 'cloud.jetpack.com';
+	return `https://${ host }/${ url }`;
+};
+
+/**
+ * Generates the URL for subscriber statistics based on site context.
+ *
+ * @param {string}  site        - The site identifier
+ * @param {boolean} isWpcomSite - Whether the site is on WordPress.com
+ * @param {string}  adminUrl    - The admin URL for self-hosted sites
+ * @returns {string} The appropriate subscriber stats URL
+ */
+export const getSubscriberStatsUrl = (
+	site: string,
+	isWpcomSite: boolean,
+	adminUrl: string
+): string => {
+	return isWpcomSite
+		? getRedirectUrl( buildJPRedirectSource( `stats/subscribers/${ site }` ) )
+		: `${ adminUrl }admin.php?page=stats#!/stats/subscribers/${ site }`;
+};
+
+/**
+ * Formats a number into a localized string representation.
+ *
+ * @param {number} num - The number to format.
+ * @returns {string} The formatted number string.
+ */
+export const formatNumber = ( num: number ): string => {
+	return num.toLocaleString();
+};
+
+/**
+ * Formats a date into a string representation.
+ *
+ * @param {Date}             date   - The date to format.
+ * @param {'short' | 'full'} format - Format type: 'short' for "Jan 5" or 'full' for "Jan 5, 2023".
+ * @returns {string} The formatted date string.
+ */
+export const formatDate = ( date: Date, format: 'short' | 'full' = 'short' ) => {
+	if ( format === 'short' ) {
+		// 'M j' = Short month name followed by day number without leading zeros (e.g., "Jan 5")
+		return dateI18n( 'M j', date );
+	}
+
+	// 'M j, Y' = Short month name, day number without leading zeros, and year (e.g., "Jan 5, 2023")
+	return dateI18n( 'M j, Y', date );
+};
+
+/**
+ * Formats a date specifically for axis tick labels.
+ *
+ * @param {Date} date - The date to format.
+ * @returns {string} The formatted date string in short format.
+ */
+export const formatAxisTickDate = ( date: Date ) => formatDate( date, 'short' );
+
+/**
+ * Calculates evenly spaced tick values for the X-axis for time series data.
+ * Assumes the data is sorted by date, earliest is first.
+ *
+ * @param {ChartSubscriptionDataPoint[]} data - The subscription data array.
+ * @returns {Date[]} An array of dates representing tick positions at 0%, 25%, 50%, 75%, and 100% of the time range.
+ */
+export const getXAxisTickValues = ( data: ChartSubscriptionDataPoint[] ) => {
+	if ( data.length < 2 ) return data.map( d => d.date );
+
+	const firstDate = data[ 0 ].date;
+	const lastDate = data[ data.length - 1 ].date;
+
+	// Calculate total time span in milliseconds
+	const timeSpan = lastDate.getTime() - firstDate.getTime();
+
+	return [
+		firstDate,
+		new Date( firstDate.getTime() + timeSpan * 0.25 ),
+		new Date( firstDate.getTime() + timeSpan * 0.5 ),
+		new Date( firstDate.getTime() + timeSpan * 0.75 ),
+		lastDate,
+	];
+};
+
+/**
+ * Transforms daily subscription counts into a format for the visx chart package.
+ *
+ * @param {Record<string, DailySubscriptionStat>} countsByDay - Object mapping date strings to daily subscription counts.
+ * @returns {ChartSubscriptionDataPoint[]} An array of subscription statistics.
+ */
+export const transformData = (
+	countsByDay: SubscriberTotalsByDate
+): ChartSubscriptionDataPoint[] => {
+	return Object.entries( countsByDay )
+		.map( ( [ dateStr, counts ] ) => {
+			const date = new Date( dateStr );
+
+			if ( isNaN( date.getTime() ) ) {
+				return null;
+			}
+
+			return {
+				date,
+				all: counts?.all ?? 0,
+				paid: counts?.paid ?? 0,
+			};
+		} )
+		.filter( Boolean )
+		.sort( ( a, b ) => a.date.getTime() - b.date.getTime() );
+};
+
+/**
+ * Calculates the maximum value of the subscription statistics to determine the left axis tick label margin.
+ * Larger labels will get cut off so we must dynamically increase the margin based on how many digits are in the largest number.
+ *
+ * @param {ChartSubscriptionDataPoint[]} subs - The subscription statistics array. The same data used to render the chart.
+ * @returns {number} The calculated left axis margin.
+ */
+export const calcLeftAxisMargin = ( subs: ChartSubscriptionDataPoint[] ): number => {
+	const DEFAULT_MARGIN = 30;
+	const CHAR_PX_WIDTH = 8;
+	const PADDING = 10;
+
+	const maxValue = Math.max( ...subs.map( d => Math.max( d.all || 0, d.paid || 0 ) ) );
+	// Estimate character width (in pixels) and calculate margin
+	// Each digit is roughly 8px, plus add some padding
+	const digitCount = maxValue.toString().length;
+	return Math.max( DEFAULT_MARGIN, digitCount * CHAR_PX_WIDTH + PADDING );
+};
diff --git a/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/index.tsx b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/index.tsx
index 8082c6ee032a..c6f4121b76e4 100644
--- a/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/index.tsx
+++ b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/index.tsx
@@ -1,12 +1,15 @@
 import * as jpDataUtils from '@automattic/jetpack-script-data';
 import { createRoot } from '@wordpress/element';
-import { NewsletterWidget } from './newsletter-widget';
+import { NewsletterWidget } from './components/newsletter-widget';
+import type { SubscriberTotalsByDate } from './types';
 
 declare global {
 	interface Window {
 		jetpackNewsletterWidgetConfigData?: {
 			emailSubscribers?: number;
 			paidSubscribers?: number;
+			allSubscribers?: number;
+			subscriberTotalsByDate?: SubscriberTotalsByDate;
 		};
 	}
 }
@@ -18,7 +21,8 @@ document.addEventListener( 'DOMContentLoaded', () => {
 		return;
 	}
 
-	const { emailSubscribers, paidSubscribers } = window.jetpackNewsletterWidgetConfigData || {};
+	const { emailSubscribers, paidSubscribers, allSubscribers, subscriberTotalsByDate } =
+		window.jetpackNewsletterWidgetConfigData || {};
 	const { suffix: site } = jpDataUtils.getSiteData();
 	const adminUrl = jpDataUtils.getAdminUrl();
 	const isWpcomSite = jpDataUtils.isWpcomPlatformSite();
@@ -35,6 +39,8 @@ document.addEventListener( 'DOMContentLoaded', () => {
 			isWpcomSite={ isWpcomSite }
 			emailSubscribers={ emailSubscribers }
 			paidSubscribers={ paidSubscribers }
+			allSubscribers={ allSubscribers }
+			subscriberTotalsByDate={ subscriberTotalsByDate }
 		/>
 	);
 } );
diff --git a/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/newsletter-widget.tsx b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/newsletter-widget.tsx
deleted file mode 100644
index 3138ccb2dde6..000000000000
--- a/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/newsletter-widget.tsx
+++ /dev/null
@@ -1,167 +0,0 @@
-import { getRedirectUrl } from '@automattic/jetpack-components';
-import './style.scss';
-import { Icon } from '@wordpress/components';
-import { createInterpolateElement } from '@wordpress/element';
-import { __ } from '@wordpress/i18n';
-import { envelope, payment } from '@wordpress/icons';
-
-interface NewsletterWidgetProps {
-	site: string;
-	adminUrl: string;
-	isWpcomSite: boolean;
-	emailSubscribers?: number;
-	paidSubscribers?: number;
-}
-
-/**
- * Helper function to build the Jetpack redirect source URL.
- * @param url         - The url to redirect to. Note: it can only be to a whitelisted domain, and query params and anchors must be passed to getRedirectUrl as arguments.
- * @param isWpcomSite - The the site on the WordPress.com platform. Simple or WoA.
- * @return The URL that can be passed to the getRedirectUrl function.
- * @example
- * const site = 'example.wordpress.com';
- * const redirectUrl = buildJPRedirectSource( `subscribers/${ site }`, true );
- *
- * <a href={ getRedirectUrl( redirectUrl ) }>Subscriber</a>;
- */
-const buildJPRedirectSource = ( url: string, isWpcomSite: boolean = true ) => {
-	const host = isWpcomSite ? 'wordpress.com' : 'cloud.jetpack.com';
-	return `https://${ host }/${ url }`;
-};
-
-export const NewsletterWidget = ( {
-	site,
-	adminUrl,
-	isWpcomSite,
-	emailSubscribers,
-	paidSubscribers,
-}: NewsletterWidgetProps ) => {
-	return (
-		<div className="newsletter-widget">
-			<div className="newsletter-widget__header">
-				<div className="newsletter-widget__stats">
-					<span className="newsletter-widget__stat-item newsletter-widget__stat-item--left">
-						<span className="newsletter-widget__icon">
-							<Icon icon={ envelope } size={ 20 } />
-						</span>
-						<span className="newsletter-widget__stat-content">
-							<span className="newsletter-widget__stat-number">{ emailSubscribers }</span>
-							<span className="newsletter-widget__stat-label">
-								<a
-									href={
-										isWpcomSite
-											? getRedirectUrl( buildJPRedirectSource( `stats/subscribers/${ site }` ) )
-											: `${ adminUrl }admin.php?page=stats#!/stats/subscribers/${ site }`
-									}
-								>
-									{ __( 'email subscriptions', 'jetpack' ) }
-								</a>
-							</span>
-						</span>
-					</span>
-					<span className="newsletter-widget__stat-item newsletter-widget__stat-item--right">
-						<span className="newsletter-widget__icon">
-							<Icon icon={ payment } size={ 20 } />
-						</span>
-						<span className="newsletter-widget__stat-content">
-							<span className="newsletter-widget__stat-number">{ paidSubscribers }</span>
-							<span className="newsletter-widget__stat-label">
-								<a
-									href={
-										isWpcomSite
-											? getRedirectUrl( buildJPRedirectSource( `stats/subscribers/${ site }` ) )
-											: `${ adminUrl }admin.php?page=stats#!/stats/subscribers/${ site }`
-									}
-								>
-									{ __( 'paid subscriptions', 'jetpack' ) }
-								</a>
-							</span>
-						</span>
-					</span>
-				</div>
-			</div>
-			<div className="newsletter-widget__footer">
-				<p className="newsletter-widget__footer-msg">
-					{ createInterpolateElement(
-						__(
-							'Effortlessly turn posts into emails with our Newsletter feature-expand your reach, engage readers, and monetize your writing. No coding required. <link>Learn more</link>',
-							'jetpack'
-						),
-						{
-							link: (
-								<a
-									href={ getRedirectUrl(
-										buildJPRedirectSource(
-											'learn/courses/newsletters-101/wordpress-com-newsletter'
-										)
-									) }
-								/>
-							),
-						}
-					) }
-				</p>
-				<div>
-					<h3>{ __( 'Quick Links', 'jetpack' ) }</h3>
-					<ul className="newsletter-widget__footer-list">
-						<li>
-							<a href={ `${ adminUrl }edit.php` }>{ __( 'Publish your next post', 'jetpack' ) }</a>
-						</li>
-						<li>
-							<a
-								href={
-									isWpcomSite
-										? getRedirectUrl( buildJPRedirectSource( `stats/subscribers/${ site }` ) )
-										: `${ adminUrl }admin.php?page=stats#!/stats/subscribers/${ site }`
-								}
-							>
-								{ __( 'View subscriber stats', 'jetpack' ) }
-							</a>
-						</li>
-						<li>
-							<a
-								href={ getRedirectUrl(
-									buildJPRedirectSource( `subscribers/${ site }`, isWpcomSite ),
-									{ anchor: 'add-subscribers' }
-								) }
-							>
-								{ __( 'Import subscribers', 'jetpack' ) }
-							</a>
-						</li>
-						<li>
-							<a
-								href={ getRedirectUrl(
-									buildJPRedirectSource( `subscribers/${ site }`, isWpcomSite )
-								) }
-							>
-								{ __( 'Manage subscribers', 'jetpack' ) }
-							</a>
-						</li>
-						<li>
-							<a
-								href={ getRedirectUrl(
-									buildJPRedirectSource(
-										`${ isWpcomSite ? 'earn' : 'monetize' }/${ site }`,
-										isWpcomSite
-									)
-								) }
-							>
-								{ __( 'Monetize', 'jetpack' ) }
-							</a>
-						</li>
-						<li>
-							<a
-								href={
-									isWpcomSite
-										? getRedirectUrl( buildJPRedirectSource( `settings/newsletter/${ site }` ) )
-										: `${ adminUrl }admin.php?page=jetpack#newsletter`
-								}
-							>
-								{ __( 'Newsletter settings', 'jetpack' ) }
-							</a>
-						</li>
-					</ul>
-				</div>
-			</div>
-		</div>
-	);
-};
diff --git a/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/style.scss b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/style.scss
index 64d046b514f5..964d75886768 100644
--- a/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/style.scss
+++ b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/style.scss
@@ -1,67 +1,123 @@
 @import '@wordpress/base-styles/colors';
+@import '@wordpress/base-styles/variables';
+
+$spacing-xs: $grid-unit-05;      // 4px
+$spacing-sm: $grid-unit-10;      // 8px
+$spacing-md: $grid-unit-15;      // 12px
+$spacing-lg: $grid-unit-20;      // 16px
 
 .newsletter-widget {
-	$spacing: 12px;
-
-	&__header{
-		padding: $spacing;
-	}
-
-	&__footer {
-		background-color: $gray-100;
-		padding: $spacing;
-	}
-
-	&__footer-msg {
-		margin: 0;
-		margin-bottom: $spacing;
-	}
-
-	&__footer-list {
-		margin: 0;
-		padding-left: $spacing;
-		list-style-type: disc;
-		columns: 2;
-	}
-
-	&__stat-number {
-		margin-left: 4px;
-	}
-
-	&__stat-label {	
-		margin-left: 4px;
-		text-decoration: none;
-	}
-
-	&__stats {
-		display: flex;
-		justify-content: space-between;
-		width: 100%;
-	}
-
-	&__icon {
-		display: inline-flex;
-		flex-direction: column;
-		align-items: flex-start;
-	}
-
-	&__stat-item {
-		display: flex;
-		align-items: center; 
-		gap: 4px;
-		text-decoration: none;
-		&--left {
-			margin-right: auto;
-		}
-
-		&--right {
-			margin-left: auto;
-		}
-	}
-
-	& a {
-		text-decoration: none;
-	}
+    &__header{
+        padding: $spacing-md;
+        margin-bottom: $spacing-md;
+    }
+
+    &__heading {
+        font-weight: $font-weight-medium !important;
+    }
+
+    &__footer {
+        background-color: $gray-100;
+        padding: $spacing-md;
+    }
+
+    &__footer-msg {
+        margin: 0;
+        margin-bottom: $spacing-md;
+    }
+
+    &__footer-list {
+        margin: 0;
+        padding-left: $spacing-md;
+        list-style-type: disc;
+        columns: 2;
+    }
+
+    &__stat-label {	
+        margin-left: $spacing-xs;
+        text-decoration: none;
+    }
+
+    &__stats {
+        display: flex;
+        gap: $spacing-md;
+        flex-wrap: wrap;
+        width: 100%;
+
+        > :first-child {
+            margin-right: $spacing-md;
+        }
+    }
+
+    &__icon {
+        display: inline-flex;
+        flex-direction: column;
+        align-items: flex-start;
+    }
+
+    &__stat-item {
+        display: flex;
+        align-items: center; 
+        gap: $spacing-xs;
+        text-decoration: none;
+    }
+
+    &__chart-container {
+        padding: 0 $spacing-md;
+    }
+
+    &__chart {
+        width: 100%;
+        box-sizing: border-box;
+        padding: 0 $spacing-md $spacing-md;
+    }
+
+    & a {
+        text-decoration: none;
+    }
+}
+
+.subscribers-chart {
+    width: 100%;
+    height: 200px;
+
+    &__tooltip {
+        background: $white;
+        padding: $spacing-sm;
+        border-radius: $radius-medium;
+        box-shadow: $elevation-x-small;
+        font-size: $font-size-small;
+        color: $gray-900;
+        min-width: 120px;
+    }
+
+    &__tooltip-date {
+        font-weight: $font-weight-medium;
+        margin-bottom: $spacing-xs;
+    }
+
+    &__tooltip-stats {
+        display: flex;
+        flex-direction: column;
+        gap: $spacing-xs / 2;
+    }
+
+    &__tooltip-stat {
+        display: flex;
+        align-items: center;
+    }
+
+    &__tooltip-indicator {
+        width: $spacing-sm;
+        height: $spacing-sm;
+        border-radius: $radius-round;
+        margin-right: $spacing-xs;
+    }
+
+    &__legend {
+        display: flex;
+        justify-content: center;
+    }
 }
 
 // wp-admin overrides
@@ -70,4 +126,4 @@
         padding: 0;
         margin: 0;
     }
-}
\ No newline at end of file
+}
diff --git a/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/types.ts b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/types.ts
new file mode 100644
index 000000000000..c0342676ffaa
--- /dev/null
+++ b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/types.ts
@@ -0,0 +1,12 @@
+export type SubscriberTotals = {
+	all: number;
+	paid: number;
+};
+
+export type SubscriberTotalsByDate = Record< string, SubscriberTotals >;
+
+export type ChartSubscriptionDataPoint = {
+	date: Date;
+	all: number;
+	paid: number;
+};
diff --git a/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/test/helpers.test.ts b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/test/helpers.test.ts
new file mode 100644
index 000000000000..263170b4d0fa
--- /dev/null
+++ b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/test/helpers.test.ts
@@ -0,0 +1,107 @@
+import { formatAxisTickDate, formatDate, getXAxisTickValues, transformData } from '../src/helpers';
+import type { SubscriberTotalsByDate, ChartSubscriptionDataPoint } from '../src/types';
+
+describe( 'helpers', () => {
+	describe( 'formatDate', () => {
+		const testDate = new Date( '2025-03-01' );
+
+		it( 'formats date in short format by default', () => {
+			const formatted = formatDate( testDate );
+
+			// Test for parts of the string rather than the exact string to handle locale differences
+			expect( formatted ).toContain( 'Mar' );
+			expect( formatted ).toContain( '1' );
+			expect( formatted ).not.toContain( '2025' );
+		} );
+
+		it( 'formats date in full format when specified', () => {
+			const formatted = formatDate( testDate, 'full' );
+
+			expect( formatted ).toContain( 'Mar' );
+			expect( formatted ).toContain( '1' );
+			expect( formatted ).toContain( '2025' );
+		} );
+	} );
+
+	describe( 'formatAxisTickDate', () => {
+		it( 'formats dates for axis ticks', () => {
+			const testDate = new Date( '2025-03-01' );
+			const formatted = formatAxisTickDate( testDate );
+
+			expect( formatted ).toBeDefined();
+			expect( typeof formatted ).toBe( 'string' );
+			expect( formatted ).toContain( 'Mar' );
+			expect( formatted ).toContain( '1' );
+		} );
+
+		it( 'uses short date format', () => {
+			const testDate = new Date( '2025-03-01' );
+			const formatted = formatAxisTickDate( testDate );
+			const shortFormatted = formatDate( testDate, 'short' );
+
+			expect( formatted ).toEqual( shortFormatted );
+		} );
+	} );
+
+	describe( 'getXAxisTickValues', () => {
+		it( 'returns all dates when there are fewer than 2 data points', () => {
+			const data: ChartSubscriptionDataPoint[] = [
+				{ date: new Date( '2025-03-05' ), all: 10, paid: 5 },
+			];
+
+			const tickValues = getXAxisTickValues( data );
+			expect( tickValues ).toHaveLength( 1 );
+			expect( tickValues[ 0 ] ).toEqual( data[ 0 ].date );
+		} );
+
+		it( 'returns empty array for empty input', () => {
+			const tickValues = getXAxisTickValues( [] );
+			expect( tickValues ).toEqual( [] );
+		} );
+
+		it( 'returns 5 evenly spaced dates across the time range', () => {
+			const startDate = new Date( '2025-01-04' );
+			const endDate = new Date( '2025-03-05' );
+
+			const data: ChartSubscriptionDataPoint[] = [
+				{ date: startDate, all: 10, paid: 5 },
+				{ date: endDate, all: 30, paid: 5 },
+			];
+
+			const tickValues = getXAxisTickValues( data );
+
+			// Should have 5 tick values
+			expect( tickValues ).toHaveLength( 5 );
+
+			// First and last should match the bounds
+			expect( tickValues[ 0 ].getTime() ).toEqual( startDate.getTime() );
+			expect( tickValues[ 4 ].getTime() ).toEqual( endDate.getTime() );
+
+			expect( tickValues[ 1 ].getTime() ).toBeCloseTo( new Date( '2025-01-19' ).getTime() );
+			expect( tickValues[ 2 ].getTime() ).toBeCloseTo( new Date( '2025-02-03' ).getTime() );
+			expect( tickValues[ 3 ].getTime() ).toBeCloseTo( new Date( '2025-02-18' ).getTime() );
+		} );
+	} );
+
+	describe( 'transformData', () => {
+		it( 'returns empty array for empty input', () => {
+			const transformedData = transformData( {} );
+			expect( transformedData ).toEqual( [] );
+		} );
+
+		it( 'sorts data by date', () => {
+			const countsByDay: SubscriberTotalsByDate = {
+				'2025-03-03': { all: 20, paid: 5 },
+				'2025-03-01': { all: 10, paid: 5 },
+				'2025-03-02': { all: 15, paid: 5 },
+			};
+
+			const transformedData = transformData( countsByDay );
+
+			// Should be sorted by date regardless of input order
+			expect( transformedData[ 0 ].date ).toEqual( new Date( '2025-03-01' ) );
+			expect( transformedData[ 1 ].date ).toEqual( new Date( '2025-03-02' ) );
+			expect( transformedData[ 2 ].date ).toEqual( new Date( '2025-03-03' ) );
+		} );
+	} );
+} );
diff --git a/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/test/index.test.tsx b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/test/index.test.tsx
index 0f820eac4812..0acabfff5d9f 100644
--- a/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/test/index.test.tsx
+++ b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/test/index.test.tsx
@@ -36,7 +36,7 @@ describe( 'Newsletter Widget Initialization', () => {
 		expect( mockRender ).not.toHaveBeenCalled();
 	} );
 
-	it( 'does not create root when config data is missing', () => {
+	it( 'does not create root when required config data is missing', () => {
 		window.JetpackScriptData = { site: { admin_url: '', suffix: '' } } as JetpackScriptData;
 		document.body.innerHTML = '<div id="newsletter-widget-app"></div>';
 
@@ -45,7 +45,7 @@ describe( 'Newsletter Widget Initialization', () => {
 		expect( mockRender ).not.toHaveBeenCalled();
 	} );
 
-	it( 'creates root and renders component when container and config are present', () => {
+	it( 'creates root and renders component when container and required config are present', () => {
 		const container = document.createElement( 'div' );
 		container.id = 'newsletter-widget-app';
 		document.body.appendChild( container );
diff --git a/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/test/newsletter-widget.test.tsx b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/test/newsletter-widget.test.tsx
index 590c3ba132ab..e534c566dae3 100644
--- a/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/test/newsletter-widget.test.tsx
+++ b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/test/newsletter-widget.test.tsx
@@ -1,8 +1,7 @@
 import { getRedirectUrl } from '@automattic/jetpack-components';
 import { render, screen } from '@testing-library/react';
-import { NewsletterWidget } from '../src/newsletter-widget';
+import { NewsletterWidget, NewsletterWidgetProps } from '../src/components/newsletter-widget';
 
-// Add these mock declarations at the top of the file, before the tests
 jest.mock( '@wordpress/components', () => {
 	const actualModule = jest.requireActual( '@wordpress/components' );
 	const mockModule = {
@@ -20,27 +19,36 @@ jest.mock( '@wordpress/icons', () => ( {
 } ) );
 
 describe( 'NewsletterWidget', () => {
-	const defaultProps = {
+	const defaultProps: NewsletterWidgetProps = {
 		site: 'example.com',
 		adminUrl: 'https://example.com/wp-admin/',
 		isWpcomSite: true,
 		emailSubscribers: 100,
+		allSubscribers: 150,
 		paidSubscribers: 50,
+		subscriberTotalsByDate: {
+			'2021-01-01': {
+				all: 10,
+				paid: 5,
+			},
+		},
 	};
 
 	it( 'renders', () => {
 		render( <NewsletterWidget { ...defaultProps } /> );
 		expect( screen.getByText( 'Quick Links' ) ).toBeInTheDocument();
-		expect( screen.getByText( defaultProps.emailSubscribers ) ).toBeInTheDocument();
-
-		// Check for paid subscribers number
-		expect( screen.getByText( defaultProps.paidSubscribers ) ).toBeInTheDocument();
 
-		// Check for email subscriptions label
-		expect( screen.getByText( 'email subscriptions' ) ).toBeInTheDocument();
+		// Check for subscriptions label
+		expect(
+			screen.getByText(
+				`${ defaultProps.allSubscribers } subscribers (${ defaultProps.emailSubscribers } via email)`
+			)
+		).toBeInTheDocument();
 
 		// Check for paid subscriptions label
-		expect( screen.getByText( 'paid subscriptions' ) ).toBeInTheDocument();
+		expect(
+			screen.getByText( `${ defaultProps.paidSubscribers } paid subscriptions` )
+		).toBeInTheDocument();
 	} );
 
 	it( 'displays the learn more link with correct href', () => {
@@ -137,4 +145,89 @@ describe( 'NewsletterWidget', () => {
 			expect( link ).toHaveAttribute( 'href', href );
 		} );
 	} );
+
+	describe( 'Stats display conditions', () => {
+		it( 'shows stats section when allSubscribers > 0', () => {
+			render(
+				<NewsletterWidget
+					{ ...defaultProps }
+					allSubscribers={ 10 }
+					emailSubscribers={ 0 }
+					paidSubscribers={ 0 }
+				/>
+			);
+
+			expect( screen.getByText( '10 subscribers (0 via email)' ) ).toBeInTheDocument();
+		} );
+
+		it( 'shows stats section when paidSubscribers > 0', () => {
+			render( <NewsletterWidget { ...defaultProps } allSubscribers={ 0 } paidSubscribers={ 5 } /> );
+
+			expect( screen.getByText( '5 paid subscriptions' ) ).toBeInTheDocument();
+		} );
+
+		it( 'hides stats section when allSubscribers and paidSubscribers are 0', () => {
+			render(
+				<NewsletterWidget
+					{ ...defaultProps }
+					allSubscribers={ 0 }
+					emailSubscribers={ 0 }
+					paidSubscribers={ 0 }
+				/>
+			);
+
+			expect( screen.queryByText( /subscribers \(\d+ via email\)/ ) ).not.toBeInTheDocument();
+
+			expect( screen.queryByText( /paid subscriptions/ ) ).not.toBeInTheDocument();
+		} );
+	} );
+
+	describe( 'Chart display conditions', () => {
+		it( 'shows chart when at least one day has a total "all" count >= 5', () => {
+			const props: NewsletterWidgetProps = {
+				...defaultProps,
+				subscriberTotalsByDate: {
+					'2021-01-01': { all: 5, paid: 0 },
+				},
+			};
+
+			render( <NewsletterWidget { ...props } /> );
+			expect( screen.getByText( 'Total Subscribers' ) ).toBeInTheDocument();
+		} );
+
+		it( 'shows chart when at least one day has a total "paid" > 0', () => {
+			const props: NewsletterWidgetProps = {
+				...defaultProps,
+				subscriberTotalsByDate: {
+					'2021-01-01': { all: 0, paid: 1 },
+				},
+			};
+
+			render( <NewsletterWidget { ...props } /> );
+			expect( screen.getByText( 'Total Subscribers' ) ).toBeInTheDocument();
+		} );
+
+		it( 'hides chart when no day has "all" count >= 5 or "paid" count > 0', () => {
+			const props: NewsletterWidgetProps = {
+				...defaultProps,
+				subscriberTotalsByDate: {
+					'2021-01-01': { all: 4, paid: 0 },
+					'2021-01-02': { all: 3, paid: 0 },
+				},
+			};
+
+			render( <NewsletterWidget { ...props } /> );
+			expect( screen.queryByText( 'Total Subscribers' ) ).not.toBeInTheDocument();
+		} );
+
+		it( 'handles empty subscriberTotalsByDate by hiding chart', () => {
+			const props = {
+				...defaultProps,
+				subscriberTotalsByDate: {},
+			};
+
+			render( <NewsletterWidget { ...props } /> );
+			expect( screen.queryByText( 'Total Subscribers' ) ).not.toBeInTheDocument();
+		} );
+	} );
 } );
diff --git a/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/test/subscribers-chart.test.tsx b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/test/subscribers-chart.test.tsx
new file mode 100644
index 000000000000..1e98a5afd20c
--- /dev/null
+++ b/projects/plugins/jetpack/modules/subscriptions/newsletter-widget/test/subscribers-chart.test.tsx
@@ -0,0 +1,30 @@
+import { render, screen } from '@testing-library/react';
+import { SubscribersChart } from '../src/components/subscribers-chart';
+import type { SubscriberTotals } from '../src/types';
+
+// Mock the ParentSize component to provide dimensions
+jest.mock( '@visx/responsive', () => ( {
+	ParentSize: ( { children } ) => children( { width: 500, height: 300 } ),
+} ) );
+
+describe( 'SubscribersChart', () => {
+	const mockCountsByDay: Record< string, SubscriberTotals > = {
+		'2023-01-01': { all: 10, paid: 5 },
+		'2023-01-02': { all: 15, paid: 5 },
+		'2023-01-03': { all: 20, paid: 8 },
+	};
+
+	it( 'renders', async () => {
+		render( <SubscribersChart subscriberTotalsByDate={ mockCountsByDay } /> );
+
+		const chart = await screen.findByLabelText( 'XYChart' );
+
+		expect( chart ).toBeInTheDocument();
+	} );
+
+	it( 'displays a message when no data is available', () => {
+		render( <SubscribersChart subscriberTotalsByDate={ {} } /> );
+
+		expect( screen.getByText( 'No data available' ) ).toBeInTheDocument();
+	} );
+} );
diff --git a/projects/plugins/jetpack/package.json b/projects/plugins/jetpack/package.json
index c0178a0d9299..fc09e5c563dc 100644
--- a/projects/plugins/jetpack/package.json
+++ b/projects/plugins/jetpack/package.json
@@ -62,6 +62,12 @@
 		"@automattic/social-previews": "2.1.0-beta.10",
 		"@automattic/viewport": "1.0.0",
 		"@microsoft/fetch-event-source": "2.0.1",
+		"@react-spring/web": "9.7.5",
+		"@visx/curve": "3.12.0",
+		"@visx/legend": "^3.12.0",
+		"@visx/responsive": "3.12.0",
+		"@visx/scale": "^3.12.0",
+		"@visx/xychart": "3.12.0",
 		"@wordpress/base-styles": "5.19.0",
 		"@wordpress/block-editor": "14.14.0",
 		"@wordpress/blocks": "14.8.0",


Reviews:
user: login: spsiddarthan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T08:17:16Z
user: login: spsiddarthan
body: Just made a tiny comment, but this works great now - tested acorss simple/atomic/jetpack.  

We should be good to merge, I suppose the only thing is what we want to display for each day - cumulative stats or stats for the day. Functionally, this works - nice work.

 I suppose we could tweak it if necessary based on what we finalize in p1741242939053579-slack-C083ZPVVDTK. 

Since I am set as the author this PR, it is not letting me approve it, but consider it approved. 🚢 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T08:21:40Z
user: login: holdercp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T21:26:52Z

Review comments:
diff_hunk: @@ -1,85 +1,91 @@
 import { getRedirectUrl } from '@automattic/jetpack-components';
-import './style.scss';
+import '../style.scss';
 import { Icon } from '@wordpress/components';
 import { createInterpolateElement } from '@wordpress/element';
-import { __ } from '@wordpress/i18n';
+import { __, sprintf } from '@wordpress/i18n';
 import { envelope, payment } from '@wordpress/icons';
+import { buildJPRedirectSource, formatNumber } from '../helpers';
+import { SubscribersChart } from './subscribers-chart';
+import type { DailyCount } from '../types';
 
 interface NewsletterWidgetProps {
 	site: string;
 	adminUrl: string;
 	isWpcomSite: boolean;
 	emailSubscribers?: number;
 	paidSubscribers?: number;
+	countsByDay?: Record< string, DailyCount >;
 }
 
-/**
- * Helper function to build the Jetpack redirect source URL.
- * @param url         - The url to redirect to. Note: it can only be to a whitelisted domain, and query params and anchors must be passed to getRedirectUrl as arguments.
- * @param isWpcomSite - The the site on the WordPress.com platform. Simple or WoA.
- * @return The URL that can be passed to the getRedirectUrl function.
- * @example
- * const site = 'example.wordpress.com';
- * const redirectUrl = buildJPRedirectSource( `subscribers/${ site }`, true );
- *
- * <a href={ getRedirectUrl( redirectUrl ) }>Subscriber</a>;
- */
-const buildJPRedirectSource = ( url: string, isWpcomSite: boolean = true ) => {
-	const host = isWpcomSite ? 'wordpress.com' : 'cloud.jetpack.com';
-	return `https://${ host }/${ url }`;
-};
-
 export const NewsletterWidget = ( {
 	site,
 	adminUrl,
 	isWpcomSite,
-	emailSubscribers,
-	paidSubscribers,
+	emailSubscribers = 0,
+	paidSubscribers = 0,
+	countsByDay = {},
 }: NewsletterWidgetProps ) => {
+	const showStats = emailSubscribers > 0 || paidSubscribers > 0;
path: projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/components/newsletter-widget.tsx
user: login: spsiddarthan
body: Shall we do `emailSubscribers > 5 || paidSubscribers > 0`? 

p1741260375720969/1741242939.053579-slack-C083ZPVVDTK
created_at: 2025-03-06T08:17:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,85 +1,91 @@
 import { getRedirectUrl } from '@automattic/jetpack-components';
-import './style.scss';
+import '../style.scss';
 import { Icon } from '@wordpress/components';
 import { createInterpolateElement } from '@wordpress/element';
-import { __ } from '@wordpress/i18n';
+import { __, sprintf } from '@wordpress/i18n';
 import { envelope, payment } from '@wordpress/icons';
+import { buildJPRedirectSource, formatNumber } from '../helpers';
+import { SubscribersChart } from './subscribers-chart';
+import type { DailyCount } from '../types';
 
 interface NewsletterWidgetProps {
 	site: string;
 	adminUrl: string;
 	isWpcomSite: boolean;
 	emailSubscribers?: number;
 	paidSubscribers?: number;
+	countsByDay?: Record< string, DailyCount >;
 }
 
-/**
- * Helper function to build the Jetpack redirect source URL.
- * @param url         - The url to redirect to. Note: it can only be to a whitelisted domain, and query params and anchors must be passed to getRedirectUrl as arguments.
- * @param isWpcomSite - The the site on the WordPress.com platform. Simple or WoA.
- * @return The URL that can be passed to the getRedirectUrl function.
- * @example
- * const site = 'example.wordpress.com';
- * const redirectUrl = buildJPRedirectSource( `subscribers/${ site }`, true );
- *
- * <a href={ getRedirectUrl( redirectUrl ) }>Subscriber</a>;
- */
-const buildJPRedirectSource = ( url: string, isWpcomSite: boolean = true ) => {
-	const host = isWpcomSite ? 'wordpress.com' : 'cloud.jetpack.com';
-	return `https://${ host }/${ url }`;
-};
-
 export const NewsletterWidget = ( {
 	site,
 	adminUrl,
 	isWpcomSite,
-	emailSubscribers,
-	paidSubscribers,
+	emailSubscribers = 0,
+	paidSubscribers = 0,
+	countsByDay = {},
 }: NewsletterWidgetProps ) => {
+	const showStats = emailSubscribers > 0 || paidSubscribers > 0;
path: projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/components/newsletter-widget.tsx
user: login: holdercp
body: I think what was described in that thread was hide the __chart__ if there are less than 5 non-paid subs and no paid subs. That's how I interpreted it. We discussed in the squad sync that we would hide the top two sections (header and chart) if there were no subs. So with that:
- We'll hide the header if there are 0 `paid` and 0 `all`.
- We'll hide the chart if there are 0 `paid` and <5 `all`.
created_at: 2025-03-06T21:26:51Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-02-27T11:38:15Z
body: <!-- wpcom-reminder-comment -->
Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

- To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate [the Jetpack Beta plugin](https://jetpack.com/download-jetpack-beta/). Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack or WordPress.com Site Helper), and enable the `add/line-chart-for-subscriber-stats` branch.
- To test on Simple, run the following command on your sandbox:
```
bin/jetpack-downloader test jetpack add/line-chart-for-subscriber-stats
```
```
bin/jetpack-downloader test jetpack-mu-wpcom-plugin add/line-chart-for-subscriber-stats
```

*Interested in more tips and information?*

- In your local development environment, use the `jetpack rsync` command to sync your changes to a WoA dev blog.
- Read more about our development workflow here: PCYsg-eg0-p2
- Figure out **when your changes will be shipped to customers** here: PCYsg-eg5-p2
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-02-27T11:38:32Z
body: **Thank you for your PR!**

When contributing to Jetpack, we have [a few suggestions](https://github.com/Automattic/jetpack/blob/trunk/.github/PULL_REQUEST_TEMPLATE.md) that can help us test and review your patch:<br>
- :white_check_mark: Include a description of your PR changes.<br>
- :white_check_mark: Add a "[Status]" label (In Progress, Needs Review, ...).<br>
- :white_check_mark: Add a "[Type]" label (Bug, Enhancement, Janitorial, Task).<br>
- :white_check_mark: Add testing instructions.<br>
- :white_check_mark: Specify whether this PR includes any changes to data or privacy.<br>
- :white_check_mark: Add changelog entries to affected projects<br>


This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation :robot:

******

**Follow this PR Review Process:**

1. Ensure all required checks appearing at the bottom of this PR are passing.
2. Make sure to test your changes on all platforms that it applies to. *You're responsible for the quality of the code you ship*.
3. You can use [GitHub's Reviewers functionality](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/requesting-a-pull-request-review) to request a review.
4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!
******

**Jetpack plugin:**

The Jetpack plugin has different release cadences depending on the platform:

- WordPress.com Simple releases happen as soon as you deploy your changes after merging this PR (PCYsg-Jjm-p2).
- WoA releases happen weekly.
- Releases to self-hosted sites happen monthly:
    - Scheduled release: _April 1, 2025_
    - Code freeze: _March 30, 2025_


If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.

author_association: CONTRIBUTOR
user: login: jp-launch-control[bot]
created_at: 2025-02-27T11:45:48Z
body: ### Code Coverage Summary

5 files are newly checked for coverage.

| File | Coverage |
| :--- | :------: |
| projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/components/subscribers-chart.tsx | 16/22 (72.73%) 💚 |
| projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/helpers.ts | 31/32 (96.88%) 💚 |
| projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/components/newsletter-widget.tsx | 5/5 (100.00%) 💚 |
| projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/constants.ts | 2/2 (100.00%) 💚 |
| projects/plugins/jetpack/modules/subscriptions/newsletter-widget/src/types.ts | 0/0 (—%) 🤷 


[Full summary](https://jetpackcodecoverage.atomicsites.blog/prs/42103/) · [PHP report](https://jetpackcodecoverage.atomicsites.blog/prs/42103/php/) · [JS report](https://jetpackcodecoverage.atomicsites.blog/prs/42103/js/)
author_association: NONE
user: login: holdercp
created_at: 2025-03-04T02:26:07Z
body: @spsiddarthan This is almost ready. I need to add tests, polish the styles, and test with different data sets. Functionally, it's pretty much there so if you need to use this branch for anything you're working on it should be fine.
author_association: CONTRIBUTOR
user: login: spsiddarthan
created_at: 2025-03-04T14:16:09Z
body: @holdercp, probably I am missing something, but we should see all 3 different colours, no? One each for paid/email/all. 

Perhaps they're over each other because most of the times no of email subs === no of all subs?

<img width="459" alt="Screenshot 2025-03-04 at 7 45 37 PM" src="https://github.com/user-attachments/assets/c25fb625-6b63-466f-83b1-6277a6023117" />


author_association: CONTRIBUTOR
user: login: holdercp
created_at: 2025-03-04T14:23:53Z
body: @spsiddarthan Ah, thanks for that catch. Re-reading the issue I see it's listed there. I'll add that series.
author_association: CONTRIBUTOR
user: login: holdercp
created_at: 2025-03-04T22:14:38Z
body: @spsiddarthan working on this today I noticed the `all` count seems to be only including email (and maybe Reader subs). Will this be fixed in Automattic/wpcom#175733?
<img width="638" alt="image" src="https://github.com/user-attachments/assets/7db69c59-caa0-4820-aa7b-c42385711f17" />

author_association: CONTRIBUTOR
user: login: spsiddarthan
created_at: 2025-03-05T06:20:43Z
body: >working on this today I noticed the all count seems to be only including email (and maybe Reader subs). 

Good find, the count has to be fixed to account for paid subs who don't have an email or reader subscription. I will fix that on the wpcom end. 

<img width="517" alt="Screenshot 2025-03-05 at 11 38 40 AM" src="https://github.com/user-attachments/assets/9dceb9fc-7366-4069-a1ab-e84ae18adbd0" />


For my usual test site on simple, atomic and jetpack, this works great. 


<img width="450" alt="Screenshot 2025-03-05 at 11 48 28 AM" src="https://github.com/user-attachments/assets/af40932e-dd5a-4ea5-bde0-4f87d6e3e82a" />

For a site with 2k+ subs, the Y axis legend doesn't look right. 
I error logged the data on wpcom, seemed ok to me. 
```
{"counts":{"email_subscribers":50,"social_followers":0,"paid_subscribers":9},"aggregate":{"2025-01-04":{"all":0,"email":0,"paid":0},"2025-01-05":{"all":0,"email":0,"paid":0},"2025-01-06":{"all":0,"email":0,"paid":0},"2025-01-07":{"all":0,"email":0,"paid":0},"2025-01-08":{"all":0,"email":0,"paid":0},"2025-01-09":{"all":0,"email":0,"paid":0},"2025-01-10":{"all":0,"email":0,"paid":0},"2025-01-11":{"all":0,"email":0,"paid":0},"2025-01-12":{"all":0,"email":0,"paid":0},"2025-01-13":{"all":0,"email":0,"paid":0},"2025-01-14":{"all":0,"email":0,"paid":0},"2025-01-15":{"all":0,"email":0,"paid":0},"2025-01-16":{"all":0,"email":0,"paid":0},"2025-01-17":{"all":0,"email":0,"paid":0},"2025-01-18":{"all":0,"email":0,"paid":0},"2025-01-19":{"all":0,"email":0,"paid":0},"2025-01-20":{"all":0,"email":0,"paid":0},"2025-01-21":{"all":0,"email":0,"paid":0},"2025-01-22":{"all":0,"email":0,"paid":0},"2025-01-23":{"all":0,"email":0,"paid":0},"2025-01-24":{"all":0,"email":0,"paid":0},"2025-01-25":{"all":0,"email":0,"paid":0},"2025-01-26":{"all":0,"email":0,"paid":0},"2025-01-27":{"all":0,"email":0,"paid":0},"2025-01-28":{"all":0,"email":0,"paid":0},"2025-01-29":{"all":1,"email":0,"paid":2},"2025-01-30":{"all":2,"email":1,"paid":1},"2025-01-31":{"all":1,"email":1,"paid":0},"2025-02-01":{"all":0,"email":0,"paid":0},"2025-02-02":{"all":0,"email":0,"paid":0},"2025-02-03":{"all":1,"email":1,"paid":0},"2025-02-04":{"all":0,"email":0,"paid":1},"2025-02-05":{"all":0,"email":0,"paid":0},"2025-02-06":{"all":0,"email":0,"paid":0},"2025-02-07":{"all":24,"email":24,"paid":0},"2025-02-08":{"all":0,"email":0,"paid":0},"2025-02-09":{"all":0,"email":0,"paid":0},"2025-02-10":{"all":0,"email":0,"paid":2},"2025-02-11":{"all":5,"email":6,"paid":5},"2025-02-12":{"all":0,"email":0,"paid":0},"2025-02-13":{"all":0,"email":0,"paid":0},"2025-02-14":{"all":12,"email":12,"paid":0},"2025-02-15":{"all":0,"email":0,"paid":0},"2025-02-16":{"all":0,"email":0,"paid":0},"2025-02-17":{"all":0,"email":0,"paid":0},"2025-02-18":{"all":0,"email":0,"paid":0},"2025-02-19":{"all":0,"email":0,"paid":0},"2025-02-20":{"all":0,"email":0,"paid":0},"2025-02-21":{"all":0,"email":0,"paid":0},"2025-02-22":{"all":0,"email":0,"paid":0},"2025-02-23":{"all":0,"email":0,"paid":0},"2025-02-24":{"all":0,"email":0,"paid":0},"2025-02-25":{"all":1,"email":1,"paid":1},"2025-02-26":{"all":0,"email":0,"paid":0},"2025-02-27":{"all":0,"email":0,"paid":0},"2025-02-28":{"all":1,"email":1,"paid":1},"2025-03-01":{"all":0,"email":0,"paid":0},"2025-03-02":{"all":0,"email":0,"paid":0},"2025-03-03":{"all":2,"email":2,"paid":2},"2025-03-04":{"all":0,"email":0,"paid":0},"2025-03-05":{"all":0,"email":0,"paid":0}}}
```


I just realized I am the author of this PR - feel free to close this and open a new one from this branch should you wish. :) 



author_association: CONTRIBUTOR
user: login: spsiddarthan
created_at: 2025-03-05T08:28:36Z
body: Sorry, had one more question:  

<img width="672" alt="Screenshot 2025-03-05 at 1 55 27 PM" src="https://github.com/user-attachments/assets/c8c0f7db-5542-4231-a3a3-bb6ab8726a80" />


On Feb 17, are we showing the total count up to Feb 17 or is it the stats on Feb 17? 

I didn't have any subs on Feb 17. `"2025-02-17":{"all":0,"email":0,"paid":0}`, so I reckon it's the sum up to Feb 17

author_association: CONTRIBUTOR
user: login: holdercp
created_at: 2025-03-05T14:29:16Z
body: @spsiddarthan 
> On Feb 17, are we showing the total count up to Feb 17 or is it the stats on Feb 17?
I didn't have any subs on Feb 17. "2025-02-17":{"all":0,"email":0,"paid":0}, so I reckon it's the sum up to Feb 17

Yeah, I'm calculating the cumulative sub count over time similar to the subscribers chart on `/subscribers`. This shows growth over time vs. the number of subs I got on a given day, which I think is a more valuable insight at a glance.

@davemart-in Any thoughts on this? Showing cumulative subscribers over time vs. subscribers gained on a given day?
author_association: CONTRIBUTOR
user: login: holdercp
created_at: 2025-03-05T14:32:58Z
body: @spsiddarthan 
>For a site with 2k+ subs, the Y axis legend doesn't look right.
I error logged the data on wpcom, seemed ok to me.

Thanks for stress testing. I only went up to 1k so it looks like there's a limit somewhere. I'll fix that now.
author_association: CONTRIBUTOR
user: login: holdercp
created_at: 2025-03-05T15:25:07Z
body: @spsiddarthan the labels were getting cut off so now we're dynamically adjusting the margin based on the label character count:
<img width="553" alt="image" src="https://github.com/user-attachments/assets/294d0e19-5e11-41fb-89cb-92864bfe055f" />

Ready for another look.

author_association: CONTRIBUTOR
user: login: spsiddarthan
created_at: 2025-03-06T13:41:09Z
body: Made a change to the header of the widget to reflect p1741264176082449/1741242939.053579-slack-C083ZPVVDTK and also changed the endpoint we call on the backend. You can sandbox Automattic/wpcom#176009 for testing Automattic/wpcom#176009 and this PR. 

We need to get rid of the 'email' line from the chart and I think we should be all set. 
author_association: CONTRIBUTOR