Skip to content

Add the Analytics page with parallel cards, a location map and a Customize sheet - #377

Open
ahamed wants to merge 21 commits into
devfrom
feat/analytics
Open

ahamed wants to merge 21 commits into
devfrom
feat/analytics

Conversation

@ahamed

@ahamed ahamed commented Oct 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the Analytics page to the admin app. Merchants see key metrics and 15 cards for a date range and an optional comparison period. They can choose which metrics and cards show, and in which order.

Backend

  • GET /analytics/metrics/{key} returns one part of the page: kpis or one of the 15 card keys. An unknown key returns 404. Each response is { meta, data }. meta has the range, the comparison, the granularity, the base currency and has_orders.
  • kpis returns 11 key metrics with change and series: Gross sales, Orders, Average order value, Returning customer rate, Net sales, Total sales, Items ordered, Discounts, New customers, Customers and Shipping charges.
  • GET /analytics/regions returns the top 5 regions of one country for Sales, Orders and Customers.
  • Ranked cards (payment methods, coupon codes, tax regions, shipping methods, locations) get the previous value and the change for each row.
  • The order seeder can make a large set of orders for analytics.

Frontend

  • Date range and comparison controls. The currency filter is removed.
  • Each card loads from its own request in parallel. It shows a skeleton until its data comes, and it shows an error with Try again when its request fails.
  • Cards:
    • over-time charts with axes and change
    • horizontal bars with growth rate for the ranked cards
    • a weekday × hour dot grid for Orders by time of day
    • a world choropleth map with region drill-down for Sales by location
    • an Inventory health card with stock tiles, badges and a live indicator
  • On the Sales by location map:
    • Hovering a country shows a border.
    • A country with no data shows "No data" and cannot be clicked.
    • The map geometry is bundled, so the page loads no remote files.
  • Customize: the Customize button opens a sheet from the right. In the sheet, the merchant can:
    • turn key metrics and cards on and off
    • drag them to a new order in their own group
    • reset to the default layout
  • Hide from page: each key metric and card also has a three-dots menu with "Hide from page".
  • Layout:
    • The layout is saved in the browser's local storage.
    • At least 1 key metric and 1 card stay on.
    • Hidden cards send no request.
  • New shared components/ui/sheet.tsx (shadcn Sheet on Radix Dialog). Its z-index layer covers the WordPress admin bar.

Docs and specs

  • docs/analytics.md describes the page, the endpoints, the cards, the Customize sheet and where this differs from WooCommerce Analytics.
  • The OpenSpec specs are synced, and the changes are archived.

Test plan

  • npm run typecheck
  • eslint
  • npm test in resources/app/ (1940 tests)
  • phpcs and phpcs:wporg
  • openspec validate (144 specs)
  • PHP integration tests (need the WordPress test library)
  • Browser check:
    • the Customize sheet over the admin bar
    • drag to reorder
    • the "Full width" note at the right of its row
    • the map hover on countries with and without data

ahamed and others added 21 commits October 18, 2026 03:25
Date range control
- Uses the shared DateRangePicker in a new confirm mode: a preset list,
  read-only Start/End fields, a two-month calendar, and a footer with the
  day count and Cancel/Apply. Future days are disabled.
- A "Custom" item is active when the range matches no preset.

Comparison control
- A popover with Previous period, Previous year, Custom, and No
  comparison. Each option shows its dates. Custom shows a range calendar.
- The footer shows "Compared with" the pending range.

Shared DateRangePicker
- New opt-in props: custom preset lists, confirm mode, and a custom
  trigger. The default behavior does not change.
- New RangeFields and RangeFooter parts. The panel separators meet the
  panel edges.

Other changes
- Add a Customize button (no function yet).
- Triggers use the white design-system background. Analytics colors are
  only for graphs.
- Remove the currency filter from the frontend and the backend. All money
  metrics use base-currency columns, the response has no currencies list,
  and a currency parameter is ignored.
- Sync the analytics-foundation specs into the main specs and add the
  analytics-date-controls OpenSpec change. Update docs/analytics.md.
- Merge the analytics-date-controls delta specs into the main specs:
  - date-pickers: custom preset lists, confirm mode, and a custom trigger.
  - analytics-page: the date range control, comparison control, Customize
    button, and the rule that analytics colors are only for graphs. Remove
    the currency control and the rounding note.
  - analytics-metrics: remove the currency filter. The endpoint ignores a
    currency parameter and returns no currencies list.
- Update the analytics-page Purpose so it does not mention a currency
  control.
- Move the change to openspec/changes/archive/2026-10-18-analytics-date-controls.
- Put the change percent and the sparkline on one row in the key metric cards.
- Add a text variant to ChangeBadge: colored text with no background and
  trending-up/down icons. The key metric cards use it; other cards keep the pill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Draw the charts at the card's pixel width, so the axis text is not
  stretched and the dots are circles.
- Y-axis: 5 round values from 0 in a short form ("$750", "$1K", "$110K").
- X-axis: one point for each day (1-31 days), week (32-186 days) or month
  (more than 186 days). Label every Nth point from the first, with
  N = ceil(points / 12), and a larger N when the labels do not fit the card.
- Tooltips show the full span of a point ("Oct 12 – Oct 18, 2026"). The API
  adds span_label and previous_span_label to the series points.
- Total sales over time: filled area under the line and a dot on the last
  point. Total sales, Average order value and Orders show a range legend.
- Show every change as colored text with trending-up/down icons, with no
  background.
- Update docs/analytics.md and the PHP and frontend tests.
- Archive the OpenSpec changes analytics-time-series-charts and
  analytics-chart-grouping-labels and sync their specs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Sales by product card on the analytics page now shows the stock
status with the shared Badge component, with the same variants as the
Top selling products card on the home dashboard:

- Out of stock: destructive
- Low stock: warning (was critical)
- Tracked with stock left: default
- In stock: success

The analytics-page spec and the page test are updated to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Remove the comparison bar and the comparison value from each category row.
- Show the change of net sales beside the amount with the ChangeBadge text.
- Add a change field to the sales_by_category rows in the overview API, the
  same as the product rows.
- Point the breakdown.bar color token at violet2 (#5A12FF).
- Update the analytics-page, analytics-metrics and theme-primitives specs,
  the analytics docs, and the tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Average order value over time card now uses the line chart in place
of the dot chart. The line has a dot on each current-period point and no
filled area under it. The comparison line stays dashed with no dots.

- Add the showPoints and showArea options to LineChart. The defaults
  keep the Total sales chart as before: a filled area and a dot on the
  last point only.
- Remove DotChart and its "dot" legend sample, which nothing uses now.
- Update the analytics-page spec and docs/analytics.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Draw the comparison range as a faded bar beside each current bar in the
Orders over time chart, in place of the short comparison mark.

- BarChart gets a showPreviousBars prop. When set, each point shows the
  previous bar (faded purple, left) and the current bar (dark purple,
  right), both with fully rounded ends.
- The legend gets a matching pairedBar sample.
- Other bar charts keep the stacked bars and the comparison mark.
- Tests, the analytics-page spec and docs/analytics.md are updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Make the segmented bar 32px high, with a 1px gap between segments and
  rounded corners only at the two ends of the bar.
- Show the delivered share in bold, with "delivered · M orders" beside it
  in a subdued color.
- Show each status row with its color dot and name, the count in bold and
  the percent in a subdued, right-aligned column.
- Update the page test, the analytics-page spec and docs/analytics.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Restyle the New vs returning customers card to match the design:

- Stack new customers (teal) under returning customers (purple) with a
  2px gap, and round only the top of the highest segment.
- Remove the comparison marks from the stacked bars and scale the
  y-axis to the current values only. The comparison values still show
  in the tooltip on hover or focus.
- Show "N customers" in the headline through a new optional unit prop
  on CardHeadline.
- Center the legend and show the totals as "New · N" and
  "Returning · N" with rounded square samples.

Update the analytics-page spec, the analytics docs and the chart and
page tests to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sales by coupon code now shows the top 5 codes by orders as horizontal
bars. Each row shows the code, the orders count, the discount given as
"$N off" in a subdued color and the change in orders against the
comparison period.

- Backend: each coupon row now has previous_orders and change. The
  coupon query moves to query_coupon_orders() so the comparison window
  uses the same query.
- HBarList takes an optional subdued detail between the value and the
  change badge.
- Tests: a PHP integration test for the coupon change and a page test
  for the new row.
- Spec and docs describe the new card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a full-width Orders by time of day card after the New vs returning
customers and Sales by coupon code row. It shows a dot for each weekday
(Mon to Sun) and hour (12am to 11pm) in the store time zone, with the
day totals on the right.

- Summary line: the busiest hour (such as "Fri 6pm–7pm"), the busiest
  day, and the share of orders from the start of the busiest hour to
  midnight (such as "After 6pm 38% of orders").
- "This period / Change" toggle, shown only with a comparison. In This
  period a bigger, darker dot means more orders. In Change the dot size
  is the absolute difference, purple for more orders and gray for fewer.
- Each dot has a tooltip, an accessible name and a fixed-size circular
  outline on hover and focus. On narrow screens the grid keeps 640px
  and scrolls inside the card.
- Backend: the overview endpoint returns orders_by_time_of_day with the
  7 × 24 placed-order counts for the range and the comparison range.
  local_time_sql() now holds the store-time conversion that
  local_date_sql() uses.
- Tests: PHP integration tests for the time zones, failed orders and
  comparison counts; Vitest tests for the summary helper and the card.
- Specs, docs and the archived OpenSpec change describe the card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ll-down

Add a full-width Sales by location card after Orders by time of day. A
world map colors each country by the selected "Sales / Orders /
Customers" tab, and a list beside it shows the top 5 countries.

- Map: d3-geo with the bundled world-atlas 110m geometry, loaded as a
  separate chunk with no remote request. Darker purple means a bigger
  value (square-root scale), gray means no value. The legend shows the
  metric, the lowest and highest value and "None".
- Hover or focus on a country with a value shows its border and a
  tooltip with the value and change. There is no wheel zoom or drag pan.
- A click on a country (on the map, with the keyboard, or on its list
  row) zooms the map to its largest land area, fades the others and
  lists its top 5 regions. "← All countries", Escape or a click on the
  sea zooms back out. The selection stays across a range change while
  the country still has a value.
- Location is the shipping address, else the billing address. Sales is
  product net sales of paid orders, Orders is placed orders, Customers
  is different customers with a paid order. Orders with no state group
  into "Unknown region".
- Backend: the overview returns sales_by_location with every country
  and its three metrics and changes. New GET /analytics/regions returns
  the top 5 regions of one country for all three metrics.
- Tests: PHP integration tests for the location rules and the regions
  endpoint (and the missing cards in the response shape test); Vitest
  tests for the color scale, the ranking and the card.
- Specs, docs and the archived OpenSpec change describe the card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sales by payment method now shows the top 5 methods by Total sales as
horizontal bars. Each row shows the method name, the Total sales, the
order count ("209 orders") in a subdued color and the change in Total
sales against the comparison period.

- Backend: each payment method row now has
  previous_total_sales_money_object and change. The payment method query
  moves to query_payment_method_sales() so the comparison window uses the
  same query.
- Frontend: the card uses HBarList, and the schema takes the two new
  optional fields.
- Tests: PHP integration tests for the change and for a method with no
  previous sales, and a page test for the new rows.
- Specs and docs describe the new card. The OpenSpec change is archived.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Taxes collected by region and Shipping charges by method cards now show
the top 5 rows as horizontal bars, with the change against the comparison
period on each row.

- Backend: each tax region row gets previous_total_tax_money_object and
  change (on Total tax). Each shipping method row gets
  previous_shipping_charges_money_object and change (on Shipping charges).
  The queries move into query_region_taxes() and
  query_shipping_method_charges() so that the comparison window uses the
  same query. The change is empty with no comparison or a previous value of 0.
- Frontend: both cards use HBarList. Shipping shows "N orders" next to the
  value. Taxes shows "Product $X · Shipping $Y" under the bar through a new
  optional HBarItem note. RankedList and RankedCardShell have no more users
  and are removed.
- Tests: PHP integration tests for the change and the new-in-range case of
  both cards, and a page test for the bars, secondary data and badges.
- Docs and specs: docs/analytics.md Cards rows, synced analytics-metrics and
  analytics-page specs, and the archived OpenSpec change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…live indicator

- Show the out-of-stock, low-stock and in-stock counts as equal tiles with a
  light background. The out-of-stock count is red, the low-stock count is
  amber, and the status name shows under each count.
- Show the variant list in a bordered box with row dividers. Each row shows
  the product, the variant and an "Out of stock" or "Low stock" badge instead
  of the quantity. The box is hidden when no variant needs attention.
- List only tracked variants that are out of stock or low stock (up to 4,
  lowest stock first). In-stock variants no longer take a place in the list.
- Add a green dot with a repeating ripple before "Live, not affected by the
  date range". The ripple stops when the user prefers reduced motion.
- Add integration and page tests, update docs/analytics.md, sync the
  analytics-metrics and analytics-page specs, and archive the OpenSpec change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Analytics page waited for one overview response that calculated every
key metric and card. Now each part has its own request, so a card shows as
soon as its data is ready.

- Backend: GET /analytics/metrics/{key} replaces GET /analytics/overview.
  The keys are kpis and the 15 card keys; an unknown key returns 404. Each
  response is { meta, data }, where meta has range, compare, granularity,
  base_currency_code and has_orders. AnalyticsQueryService::get_metric()
  builds one key; only kpis and the 5 window-based cards collect the window
  totals. AnalyticsOverviewRequest is renamed to AnalyticsMetricRequest.
- Frontend: useAnalyticsMetricQueries() runs one TanStack query per key with
  useQueries. Each card slot shows its skeleton until its data comes, and a
  card error with Try Again when its request fails. The key metrics row has
  one error for the row. The page error shows only when every request fails.
  Each card uses the meta of its own response.
- Tests: the PHP overview() helper assembles the old shape from every key,
  and AnalyticsMetricsApiTest covers one key, kpis, every key, the 404 for
  an unknown key and the removed route. Vitest covers the parallel queries,
  a card that loads first, a card that fails and a retry of one card.
- Docs and specs: docs/analytics.md lists the endpoint, the keys, the
  response shape and the request cost; analytics-metrics and analytics-page
  specs are synced, and the OpenSpec change is archived.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Hide menu

The Customize button now opens a "Customize analytics" sheet from the right.
Merchants choose which key metrics and cards the page shows, and in which
order. The choice is saved in the browser's local storage.

Sheet component
- New components/ui/sheet.tsx with the shadcn Sheet parts (Sheet,
  SheetTrigger, SheetClose, SheetContent with top/right/bottom/left sides,
  SheetHeader, SheetFooter, SheetTitle, SheetDescription) on Radix Dialog,
  styled with the design system.
- New theme.zIndex.sheet layer so the sheet and its overlay cover the
  WordPress admin bar, while menus opened inside it stay on top.

Customize sheet
- "Key metrics" and "Cards" groups in StackedItems, each item with a drag
  handle and a checkbox. Items reorder in their own group only, with the
  pointer or the keyboard. Full cards show a "Full width" note.
- Changes apply at once. "Reset to default" restores the first 4 key metrics
  and all cards in their first order. "Done" closes the sheet.
- At least 1 key metric and 1 card stay on; the last one is locked.

Page
- Each key metric and card has a three-dots menu with one item, "Hide from
  page". The button shows on hover or focus, and always on touch devices.
- Cards render from a registry in the saved order. A half card alone in its
  row fills the row.
- The key metrics row shows 4 per row; extra key metrics go to the next row
  and keep a quarter of the width.
- Hidden cards send no request.

Key metrics API
- GET /analytics/metrics/kpis now returns 11 key metrics: Net sales, Total
  sales, Items ordered, Discounts, New customers, Customers and Shipping
  charges are added, each with change and series. No new SQL; summarize()
  now carries items ordered per period for the Items ordered series.

Also
- Inventory health live indicator uses the text.success color.
- docs/analytics.md covers the Customize sheet, the 11 key metrics, the saved
  layout and the per-browser storage gap.
- OpenSpec: new sheet-primitive and analytics-customize specs; analytics-page
  and analytics-metrics updated; change archived.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Map:
- A country with no data now shows the same hover border as a country
  with data, and a tooltip with its name and "No data".
- A country with no data keeps the default cursor, does nothing on a
  click, and stays out of the keyboard order.
- Each map shape has a key with the country name as a fallback, so
  shapes without an ID get their own hover state.

Customize sheet:
- The "Full width" note sits at the right edge of its row.
- The row keeps its right padding after the note. The inline style set
  the padding on both sides and overrode it.

Docs and specs:
- docs/analytics.md and the analytics-page spec describe the new hover
  behavior. The OpenSpec change is archived.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment on lines +140 to +156
/**
* Check that a value is a real calendar day in Y-m-d format.
*
* @since 1.0.0
*
* @param string $value Value to check.
*
* @return bool
*/
protected function is_date(string $value)
{
if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $value, $parts)) {
return false;
}

return checkdate((int) $parts[2], (int) $parts[3], (int) $parts[1]);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can use date_format rule instead of this method

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants