fix merge conflicts
This commit is contained in:
commit
f5efd10638
|
@ -59,7 +59,7 @@ export const advancedFilters = {
|
|||
type: 'customers',
|
||||
getLabels: getRequestByIdString( NAMESPACE + '/customers', customer => ( {
|
||||
id: customer.id,
|
||||
label: [ customer.first_name, customer.last_name ].filter( Boolean ).join( ' ' ),
|
||||
label: customer.name,
|
||||
} ) ),
|
||||
},
|
||||
},
|
||||
|
|
|
@ -240,8 +240,8 @@ export default compose(
|
|||
return {
|
||||
tableData: {
|
||||
items: {
|
||||
data: get( revenueData, [ 'data', 'intervals' ] ),
|
||||
totalResults: get( revenueData, [ 'totalResults' ] ),
|
||||
data: get( revenueData, [ 'data', 'intervals' ], [] ),
|
||||
totalResults: get( revenueData, [ 'totalResults' ], 0 ),
|
||||
},
|
||||
isError,
|
||||
isRequesting,
|
||||
|
|
|
@ -17,6 +17,7 @@ export const filters = [
|
|||
{ label: __( 'Out of Stock', 'wc-admin' ), value: 'outofstock' },
|
||||
{ label: __( 'Low Stock', 'wc-admin' ), value: 'lowstock' },
|
||||
{ label: __( 'In Stock', 'wc-admin' ), value: 'instock' },
|
||||
{ label: __( 'On Backorder', 'wc-admin' ), value: 'onbackorder' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -103,23 +103,27 @@ export default class StockReportTable extends Component {
|
|||
}
|
||||
|
||||
getSummary( totals ) {
|
||||
const { products = 0, out_of_stock = 0, low_stock = 0, in_stock = 0 } = totals;
|
||||
const { products = 0, outofstock = 0, lowstock = 0, instock = 0, onbackorder = 0 } = totals;
|
||||
return [
|
||||
{
|
||||
label: _n( 'product', 'products', products, 'wc-admin' ),
|
||||
value: numberFormat( products ),
|
||||
},
|
||||
{
|
||||
label: __( 'out of stock', out_of_stock, 'wc-admin' ),
|
||||
value: numberFormat( out_of_stock ),
|
||||
label: __( 'out of stock', outofstock, 'wc-admin' ),
|
||||
value: numberFormat( outofstock ),
|
||||
},
|
||||
{
|
||||
label: __( 'low stock', low_stock, 'wc-admin' ),
|
||||
value: numberFormat( low_stock ),
|
||||
label: __( 'low stock', lowstock, 'wc-admin' ),
|
||||
value: numberFormat( lowstock ),
|
||||
},
|
||||
{
|
||||
label: __( 'in stock', in_stock, 'wc-admin' ),
|
||||
value: numberFormat( in_stock ),
|
||||
label: __( 'on backorder', onbackorder, 'wc-admin' ),
|
||||
value: numberFormat( onbackorder ),
|
||||
},
|
||||
{
|
||||
label: __( 'in stock', instock, 'wc-admin' ),
|
||||
value: numberFormat( instock ),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
@ -132,7 +136,7 @@ export default class StockReportTable extends Component {
|
|||
endpoint="stock"
|
||||
getHeadersContent={ this.getHeadersContent }
|
||||
getRowsContent={ this.getRowsContent }
|
||||
// getSummary={ this.getSummary }
|
||||
getSummary={ this.getSummary }
|
||||
query={ query }
|
||||
tableQuery={ {
|
||||
orderby: query.orderby || 'stock_status',
|
||||
|
|
|
@ -12,7 +12,7 @@ import PropTypes from 'prop-types';
|
|||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { getNewPath, getPersistedQuery } from '@woocommerce/navigation';
|
||||
import { getNewPath } from '@woocommerce/navigation';
|
||||
import { Link } from '@woocommerce/components';
|
||||
|
||||
/**
|
||||
|
@ -89,7 +89,7 @@ class Header extends Component {
|
|||
{ _sections.map( ( section, i ) => {
|
||||
const sectionPiece = Array.isArray( section ) ? (
|
||||
<Link
|
||||
href={ getNewPath( getPersistedQuery(), section[ 0 ], {} ) }
|
||||
href={ getNewPath( {}, section[ 0 ], {} ) }
|
||||
type={ isEmbedded ? 'wp-admin' : 'wc-admin' }
|
||||
>
|
||||
{ section[ 1 ] }
|
||||
|
|
|
@ -11,7 +11,7 @@ export const SWAGGERNAMESPACE = 'https://virtserver.swaggerhub.com/peterfabian/w
|
|||
|
||||
export const DEFAULT_REQUIREMENT = {
|
||||
timeout: 1 * MINUTE,
|
||||
freshness: 5 * MINUTE,
|
||||
freshness: 30 * MINUTE,
|
||||
};
|
||||
|
||||
// WordPress & WooCommerce both set a hard limit of 100 for the per_page parameter
|
||||
|
|
|
@ -21,6 +21,7 @@ const statEndpoints = [
|
|||
'orders',
|
||||
'products',
|
||||
'revenue',
|
||||
'stock',
|
||||
'taxes',
|
||||
'customers',
|
||||
];
|
||||
|
@ -34,6 +35,7 @@ const typeEndpointMap = {
|
|||
'report-stats-query-categories': 'categories',
|
||||
'report-stats-query-downloads': 'downloads',
|
||||
'report-stats-query-coupons': 'coupons',
|
||||
'report-stats-query-stock': 'stock',
|
||||
'report-stats-query-taxes': 'taxes',
|
||||
'report-stats-query-customers': 'customers',
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { find, forEach, isNull, get } from 'lodash';
|
||||
import { find, forEach, isNull, get, includes } from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
/**
|
||||
|
@ -52,6 +52,9 @@ export function getFilterQuery( endpoint, query ) {
|
|||
return {};
|
||||
}
|
||||
|
||||
// Some stats endpoints don't have interval data, so they can ignore after/before params and omit that part of the response.
|
||||
const noIntervalEndpoints = [ 'stock', 'customers' ];
|
||||
|
||||
/**
|
||||
* Add timestamp to advanced filter parameters involving date. The api
|
||||
* expects a timestamp for these values similar to `before` and `after`.
|
||||
|
@ -137,9 +140,10 @@ export function getQueryFromConfig( config, advancedFilters, query ) {
|
|||
* Returns true if a report object is empty.
|
||||
*
|
||||
* @param {Object} report Report to check
|
||||
* @param {String} endpoint Endpoint slug
|
||||
* @return {Boolean} True if report is data is empty.
|
||||
*/
|
||||
export function isReportDataEmpty( report ) {
|
||||
export function isReportDataEmpty( report, endpoint ) {
|
||||
if ( ! report ) {
|
||||
return true;
|
||||
}
|
||||
|
@ -149,7 +153,9 @@ export function isReportDataEmpty( report ) {
|
|||
if ( ! report.data.totals || isNull( report.data.totals ) ) {
|
||||
return true;
|
||||
}
|
||||
if ( ! report.data.intervals || 0 === report.data.intervals.length ) {
|
||||
|
||||
const checkIntervals = ! includes( noIntervalEndpoints, endpoint );
|
||||
if ( checkIntervals && ( ! report.data.intervals || 0 === report.data.intervals.length ) ) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -168,7 +174,11 @@ function getRequestQuery( endpoint, dataType, query ) {
|
|||
const interval = getIntervalForQuery( query );
|
||||
const filterQuery = getFilterQuery( endpoint, query );
|
||||
const end = datesFromQuery[ dataType ].before;
|
||||
return {
|
||||
|
||||
const noIntervals = includes( noIntervalEndpoints, endpoint );
|
||||
return noIntervals
|
||||
? { ...filterQuery }
|
||||
: {
|
||||
order: 'asc',
|
||||
interval,
|
||||
per_page: MAX_PER_PAGE,
|
||||
|
@ -250,7 +260,7 @@ export function getReportChartData( endpoint, dataType, query, select ) {
|
|||
return { ...response, isRequesting: true };
|
||||
} else if ( getReportStatsError( endpoint, requestQuery ) ) {
|
||||
return { ...response, isError: true };
|
||||
} else if ( isReportDataEmpty( stats ) ) {
|
||||
} else if ( isReportDataEmpty( stats, endpoint ) ) {
|
||||
return { ...response, isEmpty: true };
|
||||
}
|
||||
|
||||
|
|
|
@ -38,9 +38,14 @@ Current path
|
|||
|
||||
### `primaryData`
|
||||
|
||||
- **Required**
|
||||
- Type: Object
|
||||
- Default: null
|
||||
- Default: `{
|
||||
data: {
|
||||
intervals: [],
|
||||
},
|
||||
isError: false,
|
||||
isRequesting: false,
|
||||
}`
|
||||
|
||||
Primary data to display in the chart.
|
||||
|
||||
|
@ -54,9 +59,14 @@ The query string represented in object form.
|
|||
|
||||
### `secondaryData`
|
||||
|
||||
- **Required**
|
||||
- Type: Object
|
||||
- Default: null
|
||||
- Default: `{
|
||||
data: {
|
||||
intervals: [],
|
||||
},
|
||||
isError: false,
|
||||
isRequesting: false,
|
||||
}`
|
||||
|
||||
Secondary data to display in the chart.
|
||||
|
||||
|
|
|
@ -42,3 +42,17 @@ The query string represented in object form.
|
|||
|
||||
Properties of the selected chart.
|
||||
|
||||
### `summaryData`
|
||||
|
||||
- Type: Object
|
||||
- Default: `{
|
||||
totals: {
|
||||
primary: {},
|
||||
secondary: {},
|
||||
},
|
||||
isError: false,
|
||||
isRequesting: false,
|
||||
}`
|
||||
|
||||
Data to display in the SummaryNumbers.
|
||||
|
||||
|
|
|
@ -68,9 +68,8 @@ The name of the property in the item object which contains the id.
|
|||
|
||||
### `primaryData`
|
||||
|
||||
- **Required**
|
||||
- Type: Object
|
||||
- Default: null
|
||||
- Default: `{}`
|
||||
|
||||
Primary data of that report. If it's not provided, it will be automatically
|
||||
loaded via the provided `endpoint`.
|
||||
|
@ -78,7 +77,13 @@ loaded via the provided `endpoint`.
|
|||
### `tableData`
|
||||
|
||||
- Type: Object
|
||||
- Default: `{}`
|
||||
- Default: `{
|
||||
items: {
|
||||
data: [],
|
||||
totalResults: 0,
|
||||
},
|
||||
query: {},
|
||||
}`
|
||||
|
||||
Table data of that report. If it's not provided, it will be automatically
|
||||
loaded via the provided `endpoint`.
|
||||
|
|
|
@ -13,6 +13,14 @@ Props
|
|||
|
||||
Allowed intervals to show in a dropdown.
|
||||
|
||||
### `baseValue`
|
||||
|
||||
- Type: Number
|
||||
- Default: `0`
|
||||
|
||||
Base chart value. If no data value is different than the baseValue, the
|
||||
`emptyMessage` will be displayed if provided.
|
||||
|
||||
### `data`
|
||||
|
||||
- Type: Array
|
||||
|
@ -27,6 +35,14 @@ An array of data.
|
|||
|
||||
Format to parse dates into d3 time format
|
||||
|
||||
### `emptyMessage`
|
||||
|
||||
- Type: String
|
||||
- Default: null
|
||||
|
||||
The message to be displayed if there is no data to render. If no message is provided,
|
||||
nothing will be displayed.
|
||||
|
||||
### `itemsLabel`
|
||||
|
||||
- Type: String
|
||||
|
|
|
@ -7,6 +7,13 @@ A search box which autocompletes results while typing, allowing for the user to
|
|||
Props
|
||||
-----
|
||||
|
||||
### `allowFreeTextSearch`
|
||||
|
||||
- Type: Boolean
|
||||
- Default: `false`
|
||||
|
||||
Render additional options in the autocompleter to allow free text entering depending on the type.
|
||||
|
||||
### `className`
|
||||
|
||||
- Type: String
|
||||
|
@ -54,6 +61,13 @@ search box.
|
|||
|
||||
Render tags inside input, otherwise render below input.
|
||||
|
||||
### `showClearButton`
|
||||
|
||||
- Type: Boolean
|
||||
- Default: `false`
|
||||
|
||||
Render a 'Clear' button next to the input box to remove its contents.
|
||||
|
||||
### `staticResults`
|
||||
|
||||
- Type: Boolean
|
||||
|
|
|
@ -132,13 +132,6 @@ The total number of rows to display per page.
|
|||
|
||||
The string to use as a query parameter when searching row items.
|
||||
|
||||
### `searchParam`
|
||||
|
||||
- Type: String
|
||||
- Default: null
|
||||
|
||||
Url query parameter search function operates on
|
||||
|
||||
### `showMenu`
|
||||
|
||||
- Type: Boolean
|
||||
|
|
|
@ -13,46 +13,14 @@ defined( 'ABSPATH' ) || exit;
|
|||
* Customers controller.
|
||||
*
|
||||
* @package WooCommerce Admin/API
|
||||
* @extends WC_REST_Customers_Controller
|
||||
* @extends WC_Admin_REST_Reports_Customers_Controller
|
||||
*/
|
||||
class WC_Admin_REST_Customers_Controller extends WC_REST_Customers_Controller {
|
||||
|
||||
// @todo Add support for guests here. See https://wp.me/p7bje6-1dM.
|
||||
class WC_Admin_REST_Customers_Controller extends WC_Admin_REST_Reports_Customers_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
* Route base.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $namespace = 'wc/v4';
|
||||
|
||||
/**
|
||||
* Searches emails by partial search instead of a strict match.
|
||||
* See "search parameters" under https://codex.wordpress.org/Class_Reference/WP_User_Query.
|
||||
*
|
||||
* @param array $prepared_args Prepared search filter args from the customer endpoint.
|
||||
* @param array $request Request/query arguments.
|
||||
* @return array
|
||||
*/
|
||||
public static function update_search_filters( $prepared_args, $request ) {
|
||||
if ( ! empty( $request['email'] ) ) {
|
||||
$prepared_args['search'] = '*' . $prepared_args['search'] . '*';
|
||||
}
|
||||
return $prepared_args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query params for collections.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_collection_params() {
|
||||
$params = parent::get_collection_params();
|
||||
// Allow partial email matches. Previously, this was of format 'email' which required a strict "test@example.com" format.
|
||||
// This, in combination with `update_search_filters` allows us to do partial searches.
|
||||
$params['email']['format'] = '';
|
||||
return $params;
|
||||
}
|
||||
protected $rest_base = 'customers';
|
||||
}
|
||||
|
||||
add_filter( 'woocommerce_rest_customer_query', array( 'WC_Admin_REST_Customers_Controller', 'update_search_filters' ), 10, 2 );
|
||||
|
|
|
@ -46,7 +46,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
|
|||
$args['order'] = $request['order'];
|
||||
$args['orderby'] = $request['orderby'];
|
||||
$args['match'] = $request['match'];
|
||||
$args['name'] = $request['name'];
|
||||
$args['search'] = $request['search'];
|
||||
$args['username'] = $request['username'];
|
||||
$args['email'] = $request['email'];
|
||||
$args['country'] = $request['country'];
|
||||
|
@ -60,6 +60,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
|
|||
$args['avg_order_value_max'] = $request['avg_order_value_max'];
|
||||
$args['last_order_before'] = $request['last_order_before'];
|
||||
$args['last_order_after'] = $request['last_order_after'];
|
||||
$args['customers'] = $request['customers'];
|
||||
|
||||
$between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' );
|
||||
$normalized_params_numeric = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false );
|
||||
|
@ -172,7 +173,7 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
|
|||
'title' => 'report_customers',
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'customer_id' => array(
|
||||
'id' => array(
|
||||
'description' => __( 'Customer ID.', 'wc-admin' ),
|
||||
'type' => 'integer',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
|
@ -333,8 +334,8 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
|
|||
),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['name'] = array(
|
||||
'description' => __( 'Limit response to objects with a specfic customer name.', 'wc-admin' ),
|
||||
$params['search'] = array(
|
||||
'description' => __( 'Limit response to objects with a customer name containing the search term.', 'wc-admin' ),
|
||||
'type' => 'string',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
|
@ -446,6 +447,17 @@ class WC_Admin_REST_Reports_Customers_Controller extends WC_REST_Reports_Control
|
|||
'format' => 'date-time',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['customers'] = array(
|
||||
'description' => __( 'Limit result to items with specified customer ids.', 'wc-admin' ),
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'items' => array(
|
||||
'type' => 'integer',
|
||||
),
|
||||
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
|
|||
$args['registered_before'] = $request['registered_before'];
|
||||
$args['registered_after'] = $request['registered_after'];
|
||||
$args['match'] = $request['match'];
|
||||
$args['name'] = $request['name'];
|
||||
$args['search'] = $request['search'];
|
||||
$args['username'] = $request['username'];
|
||||
$args['email'] = $request['email'];
|
||||
$args['country'] = $request['country'];
|
||||
|
@ -55,6 +55,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
|
|||
$args['avg_order_value_max'] = $request['avg_order_value_max'];
|
||||
$args['last_order_before'] = $request['last_order_before'];
|
||||
$args['last_order_after'] = $request['last_order_after'];
|
||||
$args['customers'] = $request['customers'];
|
||||
|
||||
$between_params_numeric = array( 'orders_count', 'total_spend', 'avg_order_value' );
|
||||
$normalized_params_numeric = WC_Admin_Reports_Interval::normalize_between_params( $request, $between_params_numeric, false );
|
||||
|
@ -77,8 +78,6 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
|
|||
$report_data = $customers_query->get_data();
|
||||
$out_data = array(
|
||||
'totals' => $report_data,
|
||||
// @todo Is this needed? the single element array tricks the isReportDataEmpty() selector.
|
||||
'intervals' => array( (object) array() ),
|
||||
);
|
||||
|
||||
return rest_ensure_response( $out_data );
|
||||
|
@ -161,55 +160,6 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
|
|||
'readonly' => true,
|
||||
'properties' => $totals,
|
||||
),
|
||||
'intervals' => array( // @todo Remove this?
|
||||
'description' => __( 'Reports data grouped by intervals.', 'wc-admin' ),
|
||||
'type' => 'array',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
'items' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'interval' => array(
|
||||
'description' => __( 'Type of interval.', 'wc-admin' ),
|
||||
'type' => 'string',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
'enum' => array( 'day', 'week', 'month', 'year' ),
|
||||
),
|
||||
'date_start' => array(
|
||||
'description' => __( "The date the report start, in the site's timezone.", 'wc-admin' ),
|
||||
'type' => 'date-time',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'date_start_gmt' => array(
|
||||
'description' => __( 'The date the report start, as GMT.', 'wc-admin' ),
|
||||
'type' => 'date-time',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'date_end' => array(
|
||||
'description' => __( "The date the report end, in the site's timezone.", 'wc-admin' ),
|
||||
'type' => 'date-time',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'date_end_gmt' => array(
|
||||
'description' => __( 'The date the report end, as GMT.', 'wc-admin' ),
|
||||
'type' => 'date-time',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'subtotals' => array(
|
||||
'description' => __( 'Interval subtotals.', 'wc-admin' ),
|
||||
'type' => 'object',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
'properties' => $totals,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -246,7 +196,7 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
|
|||
),
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['name'] = array(
|
||||
$params['search'] = array(
|
||||
'description' => __( 'Limit response to objects with a specfic customer name.', 'wc-admin' ),
|
||||
'type' => 'string',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
|
@ -359,6 +309,16 @@ class WC_Admin_REST_Reports_Customers_Stats_Controller extends WC_REST_Reports_C
|
|||
'format' => 'date-time',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
);
|
||||
$params['customers'] = array(
|
||||
'description' => __( 'Limit result to items with specified customer ids.', 'wc-admin' ),
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => 'wp_parse_id_list',
|
||||
'validate_callback' => 'rest_validate_request_arg',
|
||||
'items' => array(
|
||||
'type' => 'integer',
|
||||
),
|
||||
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
|
|
@ -170,7 +170,7 @@ class WC_Admin_REST_Reports_Orders_Stats_Controller extends WC_Admin_REST_Report
|
|||
),
|
||||
'avg_items_per_order' => array(
|
||||
'description' => __( 'Average items per order', 'wc-admin' ),
|
||||
'type' => 'integer',
|
||||
'type' => 'number',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
<?php
|
||||
/**
|
||||
* REST API Reports stock stats controller
|
||||
*
|
||||
* Handles requests to the /reports/stock/stats endpoint.
|
||||
*
|
||||
* @package WooCommerce Admin/API
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST API Reports stock stats controller class.
|
||||
*
|
||||
* @package WooCommerce/API
|
||||
* @extends WC_REST_Reports_Controller
|
||||
*/
|
||||
class WC_Admin_REST_Reports_Stock_Stats_Controller extends WC_REST_Reports_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $namespace = 'wc/v4';
|
||||
|
||||
/**
|
||||
* Route base.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'reports/stock/stats';
|
||||
|
||||
/**
|
||||
* Get Stock Status Totals.
|
||||
*
|
||||
* @param WP_REST_Request $request Request data.
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_items( $request ) {
|
||||
$stock_query = new WC_Admin_Reports_Stock_Stats_Query();
|
||||
$report_data = $stock_query->get_data();
|
||||
$out_data = array(
|
||||
'totals' => $report_data,
|
||||
);
|
||||
return rest_ensure_response( $out_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a report object for serialization.
|
||||
*
|
||||
* @param WC_Product $report Report data.
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function prepare_item_for_response( $report, $request ) {
|
||||
$data = $report;
|
||||
|
||||
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
|
||||
$data = $this->add_additional_fields_to_object( $data, $request );
|
||||
$data = $this->filter_response_by_context( $data, $context );
|
||||
|
||||
// Wrap the data in a response object.
|
||||
$response = rest_ensure_response( $data );
|
||||
|
||||
/**
|
||||
* Filter a report returned from the API.
|
||||
*
|
||||
* Allows modification of the report data right before it is returned.
|
||||
*
|
||||
* @param WP_REST_Response $response The response object.
|
||||
* @param WC_Product $product The original bject.
|
||||
* @param WP_REST_Request $request Request used to generate the response.
|
||||
*/
|
||||
return apply_filters( 'woocommerce_rest_prepare_report_stock_stats', $response, $product, $request );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Report's schema, conforming to JSON Schema.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_item_schema() {
|
||||
$totals = array(
|
||||
'products' => array(
|
||||
'description' => __( 'Number of products.', 'wc-admin' ),
|
||||
'type' => 'integer',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'lowstock' => array(
|
||||
'description' => __( 'Number of low stock products.', 'wc-admin' ),
|
||||
'type' => 'integer',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
);
|
||||
|
||||
$status_options = wc_get_product_stock_status_options();
|
||||
foreach ( $status_options as $status => $label ) {
|
||||
$totals[ $status ] = array(
|
||||
/* translators: Stock status. Example: "Number of low stock products */
|
||||
'description' => sprintf( __( 'Number of %s products.', 'wc-admin' ), $label ),
|
||||
'type' => 'integer',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
);
|
||||
}
|
||||
|
||||
$schema = array(
|
||||
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
||||
'title' => 'report_customers_stats',
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'totals' => array(
|
||||
'description' => __( 'Totals data.', 'wc-admin' ),
|
||||
'type' => 'object',
|
||||
'context' => array( 'view', 'edit' ),
|
||||
'readonly' => true,
|
||||
'properties' => $totals,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return $this->add_additional_fields_schema( $schema );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query params for collections.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_collection_params() {
|
||||
$params = array();
|
||||
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
|
||||
return $params;
|
||||
}
|
||||
}
|
|
@ -68,6 +68,7 @@ class WC_Admin_Api_Init {
|
|||
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-downloads-stats-query.php';
|
||||
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-customers-query.php';
|
||||
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-customers-stats-query.php';
|
||||
require_once dirname( __FILE__ ) . '/class-wc-admin-reports-stock-stats-query.php';
|
||||
|
||||
// Data stores.
|
||||
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-data-store.php';
|
||||
|
@ -85,6 +86,7 @@ class WC_Admin_Api_Init {
|
|||
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-downloads-stats-data-store.php';
|
||||
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-customers-data-store.php';
|
||||
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-customers-stats-data-store.php';
|
||||
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-reports-stock-stats-data-store.php';
|
||||
|
||||
// Data triggers.
|
||||
require_once dirname( __FILE__ ) . '/data-stores/class-wc-admin-notes-data-store.php';
|
||||
|
@ -100,7 +102,6 @@ class WC_Admin_Api_Init {
|
|||
public function rest_api_init() {
|
||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-admin-notes-controller.php';
|
||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-coupons-controller.php';
|
||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-customers-controller.php';
|
||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-data-controller.php';
|
||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-data-countries-controller.php';
|
||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-data-download-ips-controller.php';
|
||||
|
@ -131,7 +132,9 @@ class WC_Admin_Api_Init {
|
|||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-controller.php';
|
||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-taxes-stats-controller.php';
|
||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-stock-controller.php';
|
||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-reports-stock-stats-controller.php';
|
||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-taxes-controller.php';
|
||||
require_once dirname( __FILE__ ) . '/api/class-wc-admin-rest-customers-controller.php';
|
||||
|
||||
$controllers = apply_filters(
|
||||
'woocommerce_admin_rest_controllers',
|
||||
|
@ -163,6 +166,7 @@ class WC_Admin_Api_Init {
|
|||
'WC_Admin_REST_Reports_Coupons_Controller',
|
||||
'WC_Admin_REST_Reports_Coupons_Stats_Controller',
|
||||
'WC_Admin_REST_Reports_Stock_Controller',
|
||||
'WC_Admin_REST_Reports_Stock_Stats_Controller',
|
||||
'WC_Admin_REST_Reports_Downloads_Controller',
|
||||
'WC_Admin_REST_Reports_Downloads_Stats_Controller',
|
||||
'WC_Admin_REST_Reports_Customers_Controller',
|
||||
|
@ -382,6 +386,7 @@ class WC_Admin_Api_Init {
|
|||
'admin-note' => 'WC_Admin_Notes_Data_Store',
|
||||
'report-customers' => 'WC_Admin_Reports_Customers_Data_Store',
|
||||
'report-customers-stats' => 'WC_Admin_Reports_Customers_Stats_Data_Store',
|
||||
'report-stock-stats' => 'WC_Admin_Reports_Stock_Stats_Data_Store',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
/**
|
||||
* Class for stock stats report querying
|
||||
*
|
||||
* $report = new WC_Admin_Reports_Stock__Stats_Query();
|
||||
* $mydata = $report->get_data();
|
||||
*
|
||||
* @package WooCommerce Admin/Classes
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* WC_Admin_Reports_Stock_Stats_Query
|
||||
*/
|
||||
class WC_Admin_Reports_Stock_Stats_Query extends WC_Admin_Reports_Query {
|
||||
|
||||
/**
|
||||
* Get product data based on the current query vars.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_data() {
|
||||
$data_store = WC_Data_Store::load( 'report-stock-stats' );
|
||||
$results = $data_store->get_data();
|
||||
return apply_filters( 'woocommerce_reports_stock_stats_query', $results );
|
||||
}
|
||||
|
||||
}
|
|
@ -25,7 +25,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
|
|||
* @var array
|
||||
*/
|
||||
protected $column_types = array(
|
||||
'customer_id' => 'intval',
|
||||
'id' => 'intval',
|
||||
'user_id' => 'intval',
|
||||
'orders_count' => 'intval',
|
||||
'total_spend' => 'floatval',
|
||||
|
@ -38,7 +38,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
|
|||
* @var array
|
||||
*/
|
||||
protected $report_columns = array(
|
||||
'customer_id' => 'customer_id',
|
||||
'id' => 'customer_id as id',
|
||||
'user_id' => 'user_id',
|
||||
'username' => 'username',
|
||||
'name' => "CONCAT_WS( ' ', first_name, last_name ) as name", // @todo What does this mean for RTL?
|
||||
|
@ -60,7 +60,7 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
|
|||
global $wpdb;
|
||||
|
||||
// Initialize some report columns that need disambiguation.
|
||||
$this->report_columns['customer_id'] = $wpdb->prefix . self::TABLE_NAME . '.customer_id';
|
||||
$this->report_columns['id'] = $wpdb->prefix . self::TABLE_NAME . '.customer_id as id';
|
||||
$this->report_columns['date_last_order'] = "MAX( {$wpdb->prefix}wc_order_stats.date_created ) as date_last_order";
|
||||
}
|
||||
|
||||
|
@ -230,8 +230,15 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
|
|||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $query_args['name'] ) ) {
|
||||
$where_clauses[] = $wpdb->prepare( "CONCAT_WS( ' ', first_name, last_name ) = %s", $query_args['name'] );
|
||||
if ( ! empty( $query_args['search'] ) ) {
|
||||
$name_like = '%' . $wpdb->esc_like( $query_args['search'] ) . '%';
|
||||
$where_clauses[] = $wpdb->prepare( "CONCAT_WS( ' ', first_name, last_name ) LIKE %s", $name_like );
|
||||
}
|
||||
|
||||
// Allow a list of customer IDs to be specified.
|
||||
if ( ! empty( $query_args['customers'] ) ) {
|
||||
$included_customers = implode( ',', $query_args['customers'] );
|
||||
$where_clauses[] = "{$customer_lookup_table}.customer_id IN ({$included_customers})";
|
||||
}
|
||||
|
||||
$numeric_params = array(
|
||||
|
@ -253,17 +260,18 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
|
|||
$subclauses = array();
|
||||
$min_param = $numeric_param . '_min';
|
||||
$max_param = $numeric_param . '_max';
|
||||
$or_equal = isset( $query_args[ $min_param ] ) && isset( $query_args[ $max_param ] ) ? '=' : '';
|
||||
|
||||
if ( isset( $query_args[ $min_param ] ) ) {
|
||||
$subclauses[] = $wpdb->prepare(
|
||||
"{$param_info['column']} >= {$param_info['format']}",
|
||||
"{$param_info['column']} >{$or_equal} {$param_info['format']}",
|
||||
$query_args[ $min_param ]
|
||||
); // WPCS: unprepared SQL ok.
|
||||
}
|
||||
|
||||
if ( isset( $query_args[ $max_param ] ) ) {
|
||||
$subclauses[] = $wpdb->prepare(
|
||||
"{$param_info['column']} <= {$param_info['format']}",
|
||||
"{$param_info['column']} <{$or_equal} {$param_info['format']}",
|
||||
$query_args[ $max_param ]
|
||||
); // WPCS: unprepared SQL ok.
|
||||
}
|
||||
|
@ -516,6 +524,24 @@ class WC_Admin_Reports_Customers_Data_Store extends WC_Admin_Reports_Data_Store
|
|||
return $customer_id ? (int) $customer_id : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the oldest orders made by a customer.
|
||||
*
|
||||
* @param int $customer_id Customer ID.
|
||||
* @return array Orders.
|
||||
*/
|
||||
public static function get_oldest_orders( $customer_id ) {
|
||||
global $wpdb;
|
||||
$orders_table = $wpdb->prefix . 'wc_order_stats';
|
||||
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT order_id, date_created FROM {$orders_table} WHERE customer_id = %d ORDER BY date_created, order_id ASC LIMIT 2",
|
||||
$customer_id
|
||||
)
|
||||
); // WPCS: unprepared SQL ok.
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the database with customer data.
|
||||
*
|
||||
|
|
|
@ -515,24 +515,57 @@ class WC_Admin_Reports_Orders_Stats_Data_Store extends WC_Admin_Reports_Data_Sto
|
|||
* @return bool
|
||||
*/
|
||||
protected static function is_returning_customer( $order ) {
|
||||
global $wpdb;
|
||||
$customer_id = WC_Admin_Reports_Customers_Data_Store::get_customer_id_by_user_id( $order->get_user_id() );
|
||||
$orders_stats_table = $wpdb->prefix . self::TABLE_NAME;
|
||||
|
||||
if ( ! $customer_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$customer_orders = $wpdb->get_var(
|
||||
$oldest_orders = WC_Admin_Reports_Customers_Data_Store::get_oldest_orders( $customer_id );
|
||||
|
||||
if ( empty( $oldest_orders ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$first_order = $oldest_orders[0];
|
||||
|
||||
// Order is older than previous first order.
|
||||
if ( $order->get_date_created() < new WC_DateTime( $first_order->date_created ) ) {
|
||||
self::set_customer_first_order( $customer_id, $order->get_id() );
|
||||
return false;
|
||||
}
|
||||
// First order date has changed and next oldest is now the first order.
|
||||
$second_order = isset( $oldest_orders[1] ) ? $oldest_orders[1] : false;
|
||||
if (
|
||||
(int) $order->get_id() === (int) $first_order->order_id &&
|
||||
$order->get_date_created() > new WC_DateTime( $first_order->date_created ) &&
|
||||
$second_order &&
|
||||
new WC_DateTime( $second_order->date_created ) < $order->get_date_created()
|
||||
) {
|
||||
self::set_customer_first_order( $customer_id, $second_order->order_id );
|
||||
return true;
|
||||
}
|
||||
|
||||
return (int) $order->get_id() !== (int) $first_order->order_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a customer's first order and all others to returning.
|
||||
*
|
||||
* @param int $customer_id Customer ID.
|
||||
* @param int $order_id Order ID.
|
||||
*/
|
||||
protected static function set_customer_first_order( $customer_id, $order_id ) {
|
||||
global $wpdb;
|
||||
$orders_stats_table = $wpdb->prefix . self::TABLE_NAME;
|
||||
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM ${orders_stats_table} WHERE customer_id = %d AND date_created < %s AND order_id != %d",
|
||||
$customer_id,
|
||||
date( 'Y-m-d H:i:s', $order->get_date_created()->getTimestamp() ),
|
||||
$order->get_id()
|
||||
"UPDATE ${orders_stats_table} SET returning_customer = CASE WHEN order_id = %d THEN false ELSE true END WHERE customer_id = %d",
|
||||
$order_id,
|
||||
$customer_id
|
||||
)
|
||||
);
|
||||
|
||||
return $customer_orders >= 1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
<?php
|
||||
/**
|
||||
* WC_Admin_Reports_Stock_Stats_Data_Store class file.
|
||||
*
|
||||
* @package WooCommerce Admin/Classes
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* WC_Reports_Stock_Stats_Data_Store.
|
||||
*/
|
||||
class WC_Admin_Reports_Stock_Stats_Data_Store extends WC_Admin_Reports_Data_Store implements WC_Admin_Reports_Data_Store_Interface {
|
||||
|
||||
/**
|
||||
* Get stock counts for the whole store.
|
||||
*
|
||||
* @param array $query Not used for the stock stats data store, but needed for the interface.
|
||||
* @return array Array of counts.
|
||||
*/
|
||||
public function get_data( $query ) {
|
||||
$report_data = array();
|
||||
$cache_expire = DAY_IN_SECONDS * 30;
|
||||
$low_stock_transient_name = 'wc_admin_stock_count_lowstock';
|
||||
$low_stock_count = get_transient( $low_stock_transient_name );
|
||||
if ( false === $low_stock_count ) {
|
||||
$low_stock_count = $this->get_low_stock_count();
|
||||
set_transient( $low_stock_transient_name, $low_stock_count, $cache_expire );
|
||||
}
|
||||
$report_data['lowstock'] = $low_stock_count;
|
||||
|
||||
$status_options = wc_get_product_stock_status_options();
|
||||
foreach ( $status_options as $status => $label ) {
|
||||
$transient_name = 'wc_admin_stock_count_' . $status;
|
||||
$count = get_transient( $transient_name );
|
||||
if ( false === $count ) {
|
||||
$count = $this->get_count( $status );
|
||||
set_transient( $transient_name, $count, $cache_expire );
|
||||
}
|
||||
$report_data[ $status ] = $count;
|
||||
}
|
||||
|
||||
$product_count_transient_name = 'wc_admin_product_count';
|
||||
$product_count = get_transient( $product_count_transient_name );
|
||||
if ( false === $product_count ) {
|
||||
$product_count = $this->get_product_count();
|
||||
set_transient( $product_count_transient_name, $product_count, $cache_expire );
|
||||
}
|
||||
$report_data['products'] = $product_count;
|
||||
return $report_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get low stock count.
|
||||
*
|
||||
* @return int Low stock count.
|
||||
*/
|
||||
private function get_low_stock_count() {
|
||||
$query_args = array();
|
||||
$query_args['post_type'] = array( 'product', 'product_variation' );
|
||||
$low_stock = absint( max( get_option( 'woocommerce_notify_low_stock_amount' ), 1 ) );
|
||||
$no_stock = absint( max( get_option( 'woocommerce_notify_no_stock_amount' ), 0 ) );
|
||||
$query_args['meta_query'] = array( // WPCS: slow query ok.
|
||||
array(
|
||||
'key' => '_manage_stock',
|
||||
'value' => 'yes',
|
||||
),
|
||||
array(
|
||||
'key' => '_stock',
|
||||
'value' => array( $no_stock, $low_stock ),
|
||||
'compare' => 'BETWEEN',
|
||||
'type' => 'NUMERIC',
|
||||
),
|
||||
array(
|
||||
'key' => '_stock_status',
|
||||
'value' => 'instock',
|
||||
),
|
||||
);
|
||||
|
||||
$query = new WP_Query();
|
||||
$query->query( $query_args );
|
||||
return intval( $query->found_posts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count for the passed in stock status.
|
||||
*
|
||||
* @param string $status Status slug.
|
||||
* @return int Count.
|
||||
*/
|
||||
private function get_count( $status ) {
|
||||
$query_args = array();
|
||||
$query_args['post_type'] = array( 'product', 'product_variation' );
|
||||
$query_args['meta_query'] = array( // WPCS: slow query ok.
|
||||
array(
|
||||
'key' => '_stock_status',
|
||||
'value' => $status,
|
||||
),
|
||||
);
|
||||
|
||||
$query = new WP_Query();
|
||||
$query->query( $query_args );
|
||||
return intval( $query->found_posts );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product count for the store.
|
||||
*
|
||||
* @return int Product count.
|
||||
*/
|
||||
private function get_product_count() {
|
||||
$query_args = array();
|
||||
$query_args['post_type'] = array( 'product', 'product_variation' );
|
||||
$query = new WP_Query();
|
||||
$query->query( $query_args );
|
||||
return intval( $query->found_posts );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the count cache when products are added or updated, or when
|
||||
* the no/low stock options are changed.
|
||||
*
|
||||
* @param int $id Post/product ID.
|
||||
*/
|
||||
function wc_admin_clear_stock_count_cache( $id ) {
|
||||
delete_transient( 'wc_admin_stock_count_lowstock' );
|
||||
delete_transient( 'wc_admin_product_count' );
|
||||
$status_options = wc_get_product_stock_status_options();
|
||||
foreach ( $status_options as $status => $label ) {
|
||||
delete_transient( 'wc_admin_stock_count_' . $status );
|
||||
}
|
||||
}
|
||||
|
||||
add_action( 'woocommerce_update_product', 'wc_admin_clear_stock_count_cache' );
|
||||
add_action( 'woocommerce_new_product', 'wc_admin_clear_stock_count_cache' );
|
||||
add_action( 'update_option_woocommerce_notify_low_stock_amount', 'wc_admin_clear_stock_count_cache' );
|
||||
add_action( 'update_option_woocommerce_notify_no_stock_amount', 'wc_admin_clear_stock_count_cache' );
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "wc-admin",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -8851,8 +8851,7 @@
|
|||
},
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
|
@ -8870,13 +8869,11 @@
|
|||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
|
@ -8889,18 +8886,15 @@
|
|||
},
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
|
@ -9003,8 +8997,7 @@
|
|||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
|
@ -9014,7 +9007,6 @@
|
|||
"is-fullwidth-code-point": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
|
@ -9027,20 +9019,17 @@
|
|||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
|
@ -9057,7 +9046,6 @@
|
|||
"mkdirp": {
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
|
@ -9130,8 +9118,7 @@
|
|||
},
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
|
@ -9141,7 +9128,6 @@
|
|||
"once": {
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
|
@ -9217,8 +9203,7 @@
|
|||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
|
@ -9248,7 +9233,6 @@
|
|||
"string-width": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
|
@ -9266,7 +9250,6 @@
|
|||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
|
@ -9305,13 +9288,11 @@
|
|||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
"bundled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "wc-admin",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.0",
|
||||
"main": "js/index.js",
|
||||
"author": "Automattic",
|
||||
"license": "GPL-2.0-or-later",
|
||||
|
@ -16,7 +16,7 @@
|
|||
"prebuild": "npm run -s install-if-deps-outdated",
|
||||
"build:packages": "node ./bin/packages/build.js",
|
||||
"build:core": "cross-env NODE_ENV=production webpack",
|
||||
"build": "npm run build:packages && npm run build:core",
|
||||
"build": "npm run build:feature-config && npm run build:packages && npm run build:core",
|
||||
"build:release": "cross-env WC_ADMIN_PHASE=plugin ./bin/build-plugin-zip.sh",
|
||||
"build:feature-config": "php bin/generate-feature-config.php",
|
||||
"postbuild": "npm run -s i18n:php && npm run -s i18n:pot",
|
||||
|
|
|
@ -6,18 +6,14 @@
|
|||
import { Component, createRef } from '@wordpress/element';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { timeFormat as d3TimeFormat, utcParse as d3UTCParse } from 'd3-time-format';
|
||||
import { timeFormat as d3TimeFormat } from 'd3-time-format';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import D3Base from './d3base';
|
||||
import {
|
||||
getDateSpaces,
|
||||
getOrderedKeys,
|
||||
getLine,
|
||||
getLineData,
|
||||
getUniqueKeys,
|
||||
getUniqueDates,
|
||||
getFormatter,
|
||||
isDataEmpty,
|
||||
|
@ -28,11 +24,11 @@ import {
|
|||
getXLineScale,
|
||||
getYMax,
|
||||
getYScale,
|
||||
getYTickOffset,
|
||||
} from './utils/scales';
|
||||
import { drawAxis, getXTicks } from './utils/axis';
|
||||
import { drawAxis } from './utils/axis';
|
||||
import { drawBars } from './utils/bar-chart';
|
||||
import { drawLines } from './utils/line-chart';
|
||||
import ChartTooltip from './utils/tooltip';
|
||||
|
||||
/**
|
||||
* A simple D3 line and bar chart component for timeseries data in React.
|
||||
|
@ -45,28 +41,89 @@ class D3Chart extends Component {
|
|||
this.tooltipRef = createRef();
|
||||
}
|
||||
|
||||
getFormatParams() {
|
||||
const { xFormat, x2Format, yFormat } = this.props;
|
||||
|
||||
return {
|
||||
xFormat: getFormatter( xFormat, d3TimeFormat ),
|
||||
x2Format: getFormatter( x2Format, d3TimeFormat ),
|
||||
yFormat: getFormatter( yFormat ),
|
||||
};
|
||||
}
|
||||
|
||||
getScaleParams( uniqueDates ) {
|
||||
const { data, height, margin, orderedKeys, chartType } = this.props;
|
||||
|
||||
const adjHeight = height - margin.top - margin.bottom;
|
||||
const adjWidth = this.getWidth() - margin.left - margin.right;
|
||||
const yMax = getYMax( data );
|
||||
const yScale = getYScale( adjHeight, yMax );
|
||||
|
||||
if ( chartType === 'line' ) {
|
||||
return {
|
||||
xScale: getXLineScale( uniqueDates, adjWidth ),
|
||||
yMax,
|
||||
yScale,
|
||||
};
|
||||
}
|
||||
|
||||
const compact = this.shouldBeCompact();
|
||||
const xScale = getXScale( uniqueDates, adjWidth, compact );
|
||||
|
||||
return {
|
||||
xGroupScale: getXGroupScale( orderedKeys, xScale, compact ),
|
||||
xScale,
|
||||
yMax,
|
||||
yScale,
|
||||
};
|
||||
}
|
||||
|
||||
getParams( uniqueDates ) {
|
||||
const { colorScheme, data, interval, mode, orderedKeys, chartType } = this.props;
|
||||
const newOrderedKeys = orderedKeys || getOrderedKeys( data );
|
||||
|
||||
return {
|
||||
colorScheme,
|
||||
interval,
|
||||
mode,
|
||||
chartType,
|
||||
uniqueDates,
|
||||
visibleKeys: newOrderedKeys.filter( key => key.visible ),
|
||||
};
|
||||
}
|
||||
|
||||
createTooltip( chart, visibleKeys ) {
|
||||
const { colorScheme, tooltipLabelFormat, tooltipPosition, tooltipTitle, tooltipValueFormat } = this.props;
|
||||
|
||||
const tooltip = new ChartTooltip();
|
||||
tooltip.ref = this.tooltipRef.current;
|
||||
tooltip.chart = chart;
|
||||
tooltip.position = tooltipPosition;
|
||||
tooltip.title = tooltipTitle;
|
||||
tooltip.labelFormat = getFormatter( tooltipLabelFormat, d3TimeFormat );
|
||||
tooltip.valueFormat = getFormatter( tooltipValueFormat );
|
||||
tooltip.visibleKeys = visibleKeys;
|
||||
tooltip.colorScheme = colorScheme;
|
||||
this.tooltip = tooltip;
|
||||
}
|
||||
|
||||
drawChart( node ) {
|
||||
const { data, margin, chartType } = this.props;
|
||||
const params = this.getParams();
|
||||
const adjParams = Object.assign( {}, params, {
|
||||
height: params.adjHeight,
|
||||
width: params.adjWidth,
|
||||
tooltip: this.tooltipRef.current,
|
||||
valueType: params.valueType,
|
||||
} );
|
||||
const { data, dateParser, margin, chartType } = this.props;
|
||||
const uniqueDates = getUniqueDates( data, dateParser );
|
||||
const formats = this.getFormatParams();
|
||||
const params = this.getParams( uniqueDates );
|
||||
const scales = this.getScaleParams( uniqueDates );
|
||||
|
||||
const g = node
|
||||
.attr( 'id', 'chart' )
|
||||
.append( 'g' )
|
||||
.attr( 'transform', `translate(${ margin.left },${ margin.top })` );
|
||||
.attr( 'transform', `translate(${ margin.left }, ${ margin.top })` );
|
||||
|
||||
const xOffset = chartType === 'line' && adjParams.uniqueDates.length <= 1
|
||||
? adjParams.width / 2
|
||||
: 0;
|
||||
this.createTooltip( g.node(), params.visibleKeys );
|
||||
|
||||
drawAxis( g, adjParams, xOffset );
|
||||
chartType === 'line' && drawLines( g, data, adjParams, xOffset );
|
||||
chartType === 'bar' && drawBars( g, data, adjParams );
|
||||
drawAxis( g, params, scales, formats, margin );
|
||||
chartType === 'line' && drawLines( g, data, params, scales, formats, this.tooltip );
|
||||
chartType === 'bar' && drawBars( g, data, params, scales, formats, this.tooltip );
|
||||
}
|
||||
|
||||
shouldBeCompact() {
|
||||
|
@ -92,75 +149,6 @@ class D3Chart extends Component {
|
|||
return Math.max( width, minimumWidth + margin.left + margin.right );
|
||||
}
|
||||
|
||||
getParams() {
|
||||
const {
|
||||
colorScheme,
|
||||
data,
|
||||
dateParser,
|
||||
height,
|
||||
interval,
|
||||
margin,
|
||||
mode,
|
||||
orderedKeys,
|
||||
tooltipPosition,
|
||||
tooltipLabelFormat,
|
||||
tooltipValueFormat,
|
||||
tooltipTitle,
|
||||
chartType,
|
||||
xFormat,
|
||||
x2Format,
|
||||
yFormat,
|
||||
valueType,
|
||||
} = this.props;
|
||||
const adjHeight = height - margin.top - margin.bottom;
|
||||
const adjWidth = this.getWidth() - margin.left - margin.right;
|
||||
const compact = this.shouldBeCompact();
|
||||
const uniqueKeys = getUniqueKeys( data );
|
||||
const newOrderedKeys = orderedKeys || getOrderedKeys( data, uniqueKeys );
|
||||
const visibleKeys = newOrderedKeys.filter( key => key.visible );
|
||||
const lineData = getLineData( data, newOrderedKeys );
|
||||
const yMax = getYMax( lineData );
|
||||
const yScale = getYScale( adjHeight, yMax );
|
||||
const parseDate = d3UTCParse( dateParser );
|
||||
const uniqueDates = getUniqueDates( lineData, parseDate );
|
||||
const xLineScale = getXLineScale( uniqueDates, adjWidth );
|
||||
const xScale = getXScale( uniqueDates, adjWidth, compact );
|
||||
const xTicks = getXTicks( uniqueDates, adjWidth, mode, interval );
|
||||
|
||||
return {
|
||||
adjHeight,
|
||||
adjWidth,
|
||||
colorScheme,
|
||||
dateSpaces: getDateSpaces( data, uniqueDates, adjWidth, xLineScale ),
|
||||
interval,
|
||||
line: getLine( xLineScale, yScale ),
|
||||
lineData,
|
||||
margin,
|
||||
mode,
|
||||
orderedKeys: newOrderedKeys,
|
||||
visibleKeys,
|
||||
parseDate,
|
||||
tooltipPosition,
|
||||
tooltipLabelFormat: getFormatter( tooltipLabelFormat, d3TimeFormat ),
|
||||
tooltipValueFormat: getFormatter( tooltipValueFormat ),
|
||||
tooltipTitle,
|
||||
chartType,
|
||||
uniqueDates,
|
||||
uniqueKeys,
|
||||
valueType,
|
||||
xFormat: getFormatter( xFormat, d3TimeFormat ),
|
||||
x2Format: getFormatter( x2Format, d3TimeFormat ),
|
||||
xGroupScale: getXGroupScale( orderedKeys, xScale, compact ),
|
||||
xLineScale,
|
||||
xTicks,
|
||||
xScale,
|
||||
yMax,
|
||||
yScale,
|
||||
yTickOffset: getYTickOffset( adjHeight, yMax ),
|
||||
yFormat: getFormatter( yFormat ),
|
||||
};
|
||||
}
|
||||
|
||||
getEmptyMessage() {
|
||||
const { baseValue, data, emptyMessage } = this.props;
|
||||
|
||||
|
@ -172,7 +160,7 @@ class D3Chart extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { className, data, height, chartType } = this.props;
|
||||
const { className, data, height, orderedKeys, chartType } = this.props;
|
||||
const computedWidth = this.getWidth();
|
||||
return (
|
||||
<div
|
||||
|
@ -182,12 +170,12 @@ class D3Chart extends Component {
|
|||
{ this.getEmptyMessage() }
|
||||
<div className="d3-chart__tooltip" ref={ this.tooltipRef } />
|
||||
<D3Base
|
||||
className={ classNames( this.props.className ) }
|
||||
className={ classNames( className ) }
|
||||
data={ data }
|
||||
drawChart={ this.drawChart }
|
||||
height={ height }
|
||||
orderedKeys={ this.props.orderedKeys }
|
||||
tooltipRef={ this.tooltipRef }
|
||||
orderedKeys={ orderedKeys }
|
||||
tooltip={ this.tooltip }
|
||||
chartType={ chartType }
|
||||
width={ computedWidth }
|
||||
/>
|
||||
|
|
|
@ -9,11 +9,6 @@ import { Component, createRef } from '@wordpress/element';
|
|||
import { isEqual, throttle } from 'lodash';
|
||||
import { select as d3Select } from 'd3-selection';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { hideTooltip } from '../utils/tooltip';
|
||||
|
||||
/**
|
||||
* Provides foundation to use D3 within React.
|
||||
*
|
||||
|
@ -30,10 +25,6 @@ export default class D3Base extends Component {
|
|||
super( props );
|
||||
|
||||
this.chartRef = createRef();
|
||||
|
||||
this.delayedScroll = throttle( () => {
|
||||
hideTooltip( this.chartRef.current, props.tooltipRef.current );
|
||||
}, 300 );
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -60,6 +51,13 @@ export default class D3Base extends Component {
|
|||
this.deleteChart();
|
||||
}
|
||||
|
||||
delayedScroll() {
|
||||
const { tooltip } = this.props;
|
||||
return throttle( () => {
|
||||
tooltip && tooltip.hide();
|
||||
}, 300 );
|
||||
}
|
||||
|
||||
deleteChart() {
|
||||
d3Select( this.chartRef.current )
|
||||
.selectAll( 'svg' )
|
||||
|
@ -95,8 +93,9 @@ export default class D3Base extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { className } = this.props;
|
||||
return (
|
||||
<div className={ classNames( 'd3-base', this.props.className ) } ref={ this.chartRef } onScroll={ this.delayedScroll } />
|
||||
<div className={ classNames( 'd3-base', className ) } ref={ this.chartRef } onScroll={ this.delayedScroll() } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -105,5 +104,6 @@ D3Base.propTypes = {
|
|||
className: PropTypes.string,
|
||||
data: PropTypes.array,
|
||||
orderedKeys: PropTypes.array, // required to detect changes in data
|
||||
tooltip: PropTypes.object,
|
||||
chartType: PropTypes.string,
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
.d3-chart__container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
svg {
|
||||
overflow: visible;
|
||||
|
@ -157,8 +158,12 @@
|
|||
|
||||
.focus-grid {
|
||||
line {
|
||||
stroke: $core-grey-light-700;
|
||||
stroke: rgba( 0, 0, 0, 0.1 );
|
||||
stroke-width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.barfocus {
|
||||
fill: rgba( 0, 0, 0, 0.1 );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ const mostPoints = 31;
|
|||
*/
|
||||
const getFactors = inputNum => {
|
||||
const numFactors = [];
|
||||
for ( let i = 1; i <= Math.floor( Math.sqrt( inputNum ) ); i += 1 ) {
|
||||
for ( let i = 1; i <= Math.floor( Math.sqrt( inputNum ) ); i++ ) {
|
||||
if ( inputNum % i === 0 ) {
|
||||
numFactors.push( i );
|
||||
inputNum / i !== i && numFactors.push( inputNum / i );
|
||||
|
@ -176,11 +176,6 @@ export const compareStrings = ( s1, s2, splitChar = new RegExp( [ ' |,' ], 'g' )
|
|||
export const getYGrids = ( yMax ) => {
|
||||
const yGrids = [];
|
||||
|
||||
// If all values are 0, yMax can become NaN.
|
||||
if ( isNaN( yMax ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for ( let i = 0; i < 4; i++ ) {
|
||||
const value = yMax > 1 ? Math.round( i / 3 * yMax ) : i / 3 * yMax;
|
||||
if ( yGrids[ yGrids.length - 1 ] !== value ) {
|
||||
|
@ -191,87 +186,90 @@ export const getYGrids = ( yMax ) => {
|
|||
return yGrids;
|
||||
};
|
||||
|
||||
export const drawAxis = ( node, params, xOffset ) => {
|
||||
const xScale = params.chartType === 'line' ? params.xLineScale : params.xScale;
|
||||
const removeDuplicateDates = ( d, i, ticks, formatter ) => {
|
||||
const removeDuplicateDates = ( d, i, ticks, formatter ) => {
|
||||
const monthDate = moment( d ).toDate();
|
||||
let prevMonth = i !== 0 ? ticks[ i - 1 ] : ticks[ i ];
|
||||
prevMonth = prevMonth instanceof Date ? prevMonth : moment( prevMonth ).toDate();
|
||||
return i === 0
|
||||
? formatter( monthDate )
|
||||
: compareStrings( formatter( prevMonth ), formatter( monthDate ) ).join( ' ' );
|
||||
};
|
||||
};
|
||||
|
||||
const yGrids = getYGrids( params.yMax === 0 ? 1 : params.yMax );
|
||||
|
||||
const ticks = params.xTicks.map( d => ( params.chartType === 'line' ? moment( d ).toDate() : d ) );
|
||||
const drawXAxis = ( node, params, scales, formats ) => {
|
||||
const height = scales.yScale.range()[ 0 ];
|
||||
let ticks = getXTicks( params.uniqueDates, scales.xScale.range()[ 1 ], params.mode, params.interval );
|
||||
if ( params.type === 'line' ) {
|
||||
ticks = ticks.map( d => moment( d ).toDate() );
|
||||
}
|
||||
|
||||
node
|
||||
.append( 'g' )
|
||||
.attr( 'class', 'axis' )
|
||||
.attr( 'aria-hidden', 'true' )
|
||||
.attr( 'transform', `translate(${ xOffset }, ${ params.height })` )
|
||||
.attr( 'transform', `translate(0, ${ height })` )
|
||||
.call(
|
||||
d3AxisBottom( xScale )
|
||||
d3AxisBottom( scales.xScale )
|
||||
.tickValues( ticks )
|
||||
.tickFormat( ( d, i ) => params.interval === 'hour'
|
||||
? params.xFormat( d instanceof Date ? d : moment( d ).toDate() )
|
||||
: removeDuplicateDates( d, i, ticks, params.xFormat ) )
|
||||
? formats.xFormat( d instanceof Date ? d : moment( d ).toDate() )
|
||||
: removeDuplicateDates( d, i, ticks, formats.xFormat ) )
|
||||
);
|
||||
|
||||
node
|
||||
.append( 'g' )
|
||||
.attr( 'class', 'axis axis-month' )
|
||||
.attr( 'aria-hidden', 'true' )
|
||||
.attr( 'transform', `translate(${ xOffset }, ${ params.height + 20 })` )
|
||||
.attr( 'transform', `translate(0, ${ height + 14 })` )
|
||||
.call(
|
||||
d3AxisBottom( xScale )
|
||||
d3AxisBottom( scales.xScale )
|
||||
.tickValues( ticks )
|
||||
.tickFormat( ( d, i ) => removeDuplicateDates( d, i, ticks, params.x2Format ) )
|
||||
)
|
||||
.call( g => g.select( '.domain' ).remove() );
|
||||
.tickFormat( ( d, i ) => removeDuplicateDates( d, i, ticks, formats.x2Format ) )
|
||||
);
|
||||
|
||||
node
|
||||
.append( 'g' )
|
||||
.attr( 'class', 'pipes' )
|
||||
.attr( 'transform', `translate(${ xOffset }, ${ params.height })` )
|
||||
.attr( 'transform', `translate(0, ${ height })` )
|
||||
.call(
|
||||
d3AxisBottom( xScale )
|
||||
d3AxisBottom( scales.xScale )
|
||||
.tickValues( ticks )
|
||||
.tickSize( 5 )
|
||||
.tickFormat( '' )
|
||||
);
|
||||
};
|
||||
|
||||
const drawYAxis = ( node, scales, formats, margin ) => {
|
||||
const yGrids = getYGrids( scales.yScale.domain()[ 1 ] );
|
||||
const width = scales.xScale.range()[ 1 ];
|
||||
|
||||
if ( yGrids ) {
|
||||
node
|
||||
.append( 'g' )
|
||||
.attr( 'class', 'grid' )
|
||||
.attr( 'transform', `translate(-${ params.margin.left }, 0)` )
|
||||
.attr( 'transform', `translate(-${ margin.left }, 0)` )
|
||||
.call(
|
||||
d3AxisLeft( params.yScale )
|
||||
d3AxisLeft( scales.yScale )
|
||||
.tickValues( yGrids )
|
||||
.tickSize( -params.width - params.margin.left - params.margin.right )
|
||||
.tickSize( -width - margin.left - margin.right )
|
||||
.tickFormat( '' )
|
||||
)
|
||||
.call( g => g.select( '.domain' ).remove() );
|
||||
);
|
||||
|
||||
node
|
||||
.append( 'g' )
|
||||
.attr( 'class', 'axis y-axis' )
|
||||
.attr( 'aria-hidden', 'true' )
|
||||
.attr( 'transform', 'translate(-50, 0)' )
|
||||
.attr( 'transform', 'translate(-50, 12)' )
|
||||
.attr( 'text-anchor', 'start' )
|
||||
.call(
|
||||
d3AxisLeft( params.yTickOffset )
|
||||
.tickValues( params.yMax === 0 ? [ yGrids[ 0 ] ] : yGrids )
|
||||
.tickFormat( d => params.yFormat( d !== 0 ? d : 0 ) )
|
||||
d3AxisLeft( scales.yScale )
|
||||
.tickValues( scales.yMax === 0 ? [ yGrids[ 0 ] ] : yGrids )
|
||||
.tickFormat( d => formats.yFormat( d !== 0 ? d : 0 ) )
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const drawAxis = ( node, params, scales, formats, margin ) => {
|
||||
drawXAxis( node, params, scales, formats );
|
||||
drawYAxis( node, scales, formats, margin );
|
||||
|
||||
node.selectAll( '.domain' ).remove();
|
||||
node
|
||||
.selectAll( '.axis' )
|
||||
.selectAll( '.tick' )
|
||||
.select( 'line' )
|
||||
.remove();
|
||||
node.selectAll( '.axis .tick line' ).remove();
|
||||
};
|
||||
|
|
|
@ -4,23 +4,16 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { get } from 'lodash';
|
||||
import { event as d3Event, select as d3Select } from 'd3-selection';
|
||||
import { event as d3Event } from 'd3-selection';
|
||||
import moment from 'moment';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getColor } from './color';
|
||||
import { calculateTooltipPosition, hideTooltip, showTooltip } from './tooltip';
|
||||
|
||||
const handleMouseOverBarChart = ( date, parentNode, node, data, params, position ) => {
|
||||
d3Select( parentNode )
|
||||
.select( '.barfocus' )
|
||||
.attr( 'opacity', '0.1' );
|
||||
showTooltip( params, data.find( e => e.date === date ), position );
|
||||
};
|
||||
|
||||
export const drawBars = ( node, data, params ) => {
|
||||
export const drawBars = ( node, data, params, scales, formats, tooltip ) => {
|
||||
const height = scales.yScale.range()[ 0 ];
|
||||
const barGroup = node
|
||||
.append( 'g' )
|
||||
.attr( 'class', 'bars' )
|
||||
|
@ -28,14 +21,14 @@ export const drawBars = ( node, data, params ) => {
|
|||
.data( data )
|
||||
.enter()
|
||||
.append( 'g' )
|
||||
.attr( 'transform', d => `translate(${ params.xScale( d.date ) },0)` )
|
||||
.attr( 'transform', d => `translate(${ scales.xScale( d.date ) }, 0)` )
|
||||
.attr( 'class', 'bargroup' )
|
||||
.attr( 'role', 'region' )
|
||||
.attr(
|
||||
'aria-label',
|
||||
d =>
|
||||
params.mode === 'item-comparison'
|
||||
? params.tooltipLabelFormat( d.date instanceof Date ? d.date : moment( d.date ).toDate() )
|
||||
? tooltip.labelFormat( d.date instanceof Date ? d.date : moment( d.date ).toDate() )
|
||||
: null
|
||||
);
|
||||
|
||||
|
@ -44,23 +37,18 @@ export const drawBars = ( node, data, params ) => {
|
|||
.attr( 'class', 'barfocus' )
|
||||
.attr( 'x', 0 )
|
||||
.attr( 'y', 0 )
|
||||
.attr( 'width', params.xGroupScale.range()[ 1 ] )
|
||||
.attr( 'height', params.height )
|
||||
.attr( 'width', scales.xGroupScale.range()[ 1 ] )
|
||||
.attr( 'height', height )
|
||||
.attr( 'opacity', '0' )
|
||||
.on( 'mouseover', ( d, i, nodes ) => {
|
||||
const position = calculateTooltipPosition(
|
||||
d3Event.target,
|
||||
node.node(),
|
||||
params.tooltipPosition
|
||||
);
|
||||
handleMouseOverBarChart( d.date, nodes[ i ].parentNode, node, data, params, position );
|
||||
tooltip.show( data.find( e => e.date === d.date ), d3Event.target, nodes[ i ].parentNode );
|
||||
} )
|
||||
.on( 'mouseout', ( d, i, nodes ) => hideTooltip( nodes[ i ].parentNode, params.tooltip ) );
|
||||
.on( 'mouseout', () => tooltip.hide() );
|
||||
|
||||
barGroup
|
||||
.selectAll( '.bar' )
|
||||
.data( d =>
|
||||
params.orderedKeys.filter( row => row.visible ).map( row => ( {
|
||||
params.visibleKeys.map( row => ( {
|
||||
key: row.key,
|
||||
focus: row.focus,
|
||||
value: get( d, [ row.key, 'value' ], 0 ),
|
||||
|
@ -72,16 +60,16 @@ export const drawBars = ( node, data, params ) => {
|
|||
.enter()
|
||||
.append( 'rect' )
|
||||
.attr( 'class', 'bar' )
|
||||
.attr( 'x', d => params.xGroupScale( d.key ) )
|
||||
.attr( 'y', d => params.yScale( d.value ) )
|
||||
.attr( 'width', params.xGroupScale.bandwidth() )
|
||||
.attr( 'height', d => params.height - params.yScale( d.value ) )
|
||||
.attr( 'x', d => scales.xGroupScale( d.key ) )
|
||||
.attr( 'y', d => scales.yScale( d.value ) )
|
||||
.attr( 'width', scales.xGroupScale.bandwidth() )
|
||||
.attr( 'height', d => height - scales.yScale( d.value ) )
|
||||
.attr( 'fill', d => getColor( d.key, params.visibleKeys, params.colorScheme ) )
|
||||
.attr( 'pointer-events', 'none' )
|
||||
.attr( 'tabindex', '0' )
|
||||
.attr( 'aria-label', d => {
|
||||
const label = params.mode === 'time-comparison' && d.label ? d.label : d.key;
|
||||
return `${ label } ${ params.tooltipValueFormat( d.value ) }`;
|
||||
return `${ label } ${ tooltip.valueFormat( d.value ) }`;
|
||||
} )
|
||||
.style( 'opacity', d => {
|
||||
const opacity = d.focus ? 1 : 0.1;
|
||||
|
@ -89,8 +77,7 @@ export const drawBars = ( node, data, params ) => {
|
|||
} )
|
||||
.on( 'focus', ( d, i, nodes ) => {
|
||||
const targetNode = d.value > 0 ? d3Event.target : d3Event.target.parentNode;
|
||||
const position = calculateTooltipPosition( targetNode, node.node(), params.tooltipPosition );
|
||||
handleMouseOverBarChart( d.date, nodes[ i ].parentNode, node, data, params, position );
|
||||
tooltip.show( data.find( e => e.date === d.date ), targetNode, nodes[ i ].parentNode );
|
||||
} )
|
||||
.on( 'blur', ( d, i, nodes ) => hideTooltip( nodes[ i ].parentNode, params.tooltip ) );
|
||||
.on( 'blur', () => tooltip.hide() );
|
||||
};
|
||||
|
|
|
@ -3,10 +3,9 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { find, get, isNil } from 'lodash';
|
||||
import { isNil } from 'lodash';
|
||||
import { format as d3Format } from 'd3-format';
|
||||
import { line as d3Line } from 'd3-shape';
|
||||
import moment from 'moment';
|
||||
import { utcParse as d3UTCParse } from 'd3-time-format';
|
||||
|
||||
/**
|
||||
* Allows an overriding formatter or defaults to d3Format or d3TimeFormat
|
||||
|
@ -17,30 +16,18 @@ import moment from 'moment';
|
|||
export const getFormatter = ( format, formatter = d3Format ) =>
|
||||
typeof format === 'function' ? format : formatter( format );
|
||||
|
||||
/**
|
||||
* Describes `getUniqueKeys`
|
||||
* @param {array} data - The chart component's `data` prop.
|
||||
* @returns {array} of unique category keys
|
||||
*/
|
||||
export const getUniqueKeys = data => {
|
||||
return [
|
||||
...new Set(
|
||||
data.reduce( ( accum, curr ) => {
|
||||
Object.keys( curr ).forEach( key => key !== 'date' && accum.push( key ) );
|
||||
return accum;
|
||||
}, [] )
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes `getOrderedKeys`
|
||||
* @param {array} data - The chart component's `data` prop.
|
||||
* @param {array} uniqueKeys - from `getUniqueKeys`.
|
||||
* @returns {array} of unique category keys ordered by cumulative total value
|
||||
*/
|
||||
export const getOrderedKeys = ( data, uniqueKeys ) =>
|
||||
uniqueKeys
|
||||
export const getOrderedKeys = ( data ) => {
|
||||
const keys = new Set(
|
||||
data.reduce( ( acc, curr ) => acc.concat( Object.keys( curr ) ), [] )
|
||||
);
|
||||
|
||||
return [ ...keys ]
|
||||
.filter( key => key !== 'date' )
|
||||
.map( key => ( {
|
||||
key,
|
||||
focus: true,
|
||||
|
@ -48,93 +35,21 @@ export const getOrderedKeys = ( data, uniqueKeys ) =>
|
|||
visible: true,
|
||||
} ) )
|
||||
.sort( ( a, b ) => b.total - a.total );
|
||||
|
||||
/**
|
||||
* Describes `getLineData`
|
||||
* @param {array} data - The chart component's `data` prop.
|
||||
* @param {array} orderedKeys - from `getOrderedKeys`.
|
||||
* @returns {array} an array objects with a category `key` and an array of `values` with `date` and `value` properties
|
||||
*/
|
||||
export const getLineData = ( data, orderedKeys ) =>
|
||||
orderedKeys.map( row => ( {
|
||||
key: row.key,
|
||||
focus: row.focus,
|
||||
visible: row.visible,
|
||||
values: data.map( d => ( {
|
||||
date: d.date,
|
||||
focus: row.focus,
|
||||
label: get( d, [ row.key, 'label' ], '' ),
|
||||
value: get( d, [ row.key, 'value' ], 0 ),
|
||||
visible: row.visible,
|
||||
} ) ),
|
||||
} ) );
|
||||
|
||||
/**
|
||||
* Describes `getUniqueDates`
|
||||
* @param {array} lineData - from `GetLineData`
|
||||
* @param {function} parseDate - D3 time format parser
|
||||
* @returns {array} an array of unique date values sorted from earliest to latest
|
||||
*/
|
||||
export const getUniqueDates = ( lineData, parseDate ) => {
|
||||
return [
|
||||
...new Set(
|
||||
lineData.reduce( ( accum, { values } ) => {
|
||||
values.forEach( ( { date } ) => accum.push( date ) );
|
||||
return accum;
|
||||
}, [] )
|
||||
),
|
||||
].sort( ( a, b ) => parseDate( a ) - parseDate( b ) );
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes getLine
|
||||
* @param {function} xLineScale - from `getXLineScale`.
|
||||
* @param {function} yScale - from `getYScale`.
|
||||
* @returns {function} the D3 line function for plotting all category values
|
||||
* Describes `getUniqueDates`
|
||||
* @param {array} data - the chart component's `data` prop.
|
||||
* @param {string} dateParser - D3 time format
|
||||
* @returns {array} an array of unique date values sorted from earliest to latest
|
||||
*/
|
||||
export const getLine = ( xLineScale, yScale ) =>
|
||||
d3Line()
|
||||
.x( d => xLineScale( moment( d.date ).toDate() ) )
|
||||
.y( d => yScale( d.value ) );
|
||||
|
||||
/**
|
||||
* Describes getDateSpaces
|
||||
* @param {array} data - The chart component's `data` prop.
|
||||
* @param {array} uniqueDates - from `getUniqueDates`
|
||||
* @param {number} width - calculated width of the charting space
|
||||
* @param {function} xLineScale - from `getXLineScale`
|
||||
* @returns {array} that icnludes the date, start (x position) and width to mode the mouseover rectangles
|
||||
*/
|
||||
export const getDateSpaces = ( data, uniqueDates, width, xLineScale ) =>
|
||||
uniqueDates.map( ( d, i ) => {
|
||||
const datapoints = find( data, { date: d } );
|
||||
const xNow = xLineScale( moment( d ).toDate() );
|
||||
const xPrev =
|
||||
i >= 1
|
||||
? xLineScale( moment( uniqueDates[ i - 1 ] ).toDate() )
|
||||
: xLineScale( moment( uniqueDates[ 0 ] ).toDate() );
|
||||
const xNext =
|
||||
i < uniqueDates.length - 1
|
||||
? xLineScale( moment( uniqueDates[ i + 1 ] ).toDate() )
|
||||
: xLineScale( moment( uniqueDates[ uniqueDates.length - 1 ] ).toDate() );
|
||||
let xWidth = i === 0 ? xNext - xNow : xNow - xPrev;
|
||||
const xStart = i === 0 ? 0 : xNow - xWidth / 2;
|
||||
xWidth = i === 0 || i === uniqueDates.length - 1 ? xWidth / 2 : xWidth;
|
||||
return {
|
||||
date: d,
|
||||
start: uniqueDates.length > 1 ? xStart : 0,
|
||||
width: uniqueDates.length > 1 ? xWidth : width,
|
||||
values: Object.keys( datapoints )
|
||||
.filter( key => key !== 'date' )
|
||||
.map( key => {
|
||||
return {
|
||||
key,
|
||||
value: datapoints[ key ].value,
|
||||
date: d,
|
||||
};
|
||||
} ),
|
||||
};
|
||||
} );
|
||||
export const getUniqueDates = ( data, dateParser ) => {
|
||||
const parseDate = d3UTCParse( dateParser );
|
||||
const dates = new Set(
|
||||
data.map( d => d.date )
|
||||
);
|
||||
return [ ...dates ].sort( ( a, b ) => parseDate( a ) - parseDate( b ) );
|
||||
};
|
||||
|
||||
/**
|
||||
* Check whether data is empty.
|
||||
|
|
|
@ -3,38 +3,107 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { event as d3Event, select as d3Select } from 'd3-selection';
|
||||
import { smallBreak, wideBreak } from './breakpoints';
|
||||
import { event as d3Event } from 'd3-selection';
|
||||
import { line as d3Line } from 'd3-shape';
|
||||
import moment from 'moment';
|
||||
import { first, get } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getColor } from './color';
|
||||
import { calculateTooltipPosition, hideTooltip, showTooltip } from './tooltip';
|
||||
import { smallBreak, wideBreak } from './breakpoints';
|
||||
|
||||
const handleMouseOverLineChart = ( date, parentNode, node, data, params, position ) => {
|
||||
d3Select( parentNode )
|
||||
.select( '.focus-grid' )
|
||||
.attr( 'opacity', '1' );
|
||||
showTooltip( params, data.find( e => e.date === date ), position );
|
||||
};
|
||||
/**
|
||||
* Describes getDateSpaces
|
||||
* @param {array} data - The chart component's `data` prop.
|
||||
* @param {array} uniqueDates - from `getUniqueDates`
|
||||
* @param {number} width - calculated width of the charting space
|
||||
* @param {function} xScale - from `getXLineScale`
|
||||
* @returns {array} that includes the date, start (x position) and width to mode the mouseover rectangles
|
||||
*/
|
||||
export const getDateSpaces = ( data, uniqueDates, width, xScale ) =>
|
||||
uniqueDates.map( ( d, i ) => {
|
||||
const datapoints = first( data.filter( item => item.date === d ) );
|
||||
const xNow = xScale( moment( d ).toDate() );
|
||||
const xPrev =
|
||||
i >= 1
|
||||
? xScale( moment( uniqueDates[ i - 1 ] ).toDate() )
|
||||
: xScale( moment( uniqueDates[ 0 ] ).toDate() );
|
||||
const xNext =
|
||||
i < uniqueDates.length - 1
|
||||
? xScale( moment( uniqueDates[ i + 1 ] ).toDate() )
|
||||
: xScale( moment( uniqueDates[ uniqueDates.length - 1 ] ).toDate() );
|
||||
let xWidth = i === 0 ? xNext - xNow : xNow - xPrev;
|
||||
const xStart = i === 0 ? 0 : xNow - xWidth / 2;
|
||||
xWidth = i === 0 || i === uniqueDates.length - 1 ? xWidth / 2 : xWidth;
|
||||
return {
|
||||
date: d,
|
||||
start: uniqueDates.length > 1 ? xStart : 0,
|
||||
width: uniqueDates.length > 1 ? xWidth : width,
|
||||
values: Object.keys( datapoints )
|
||||
.filter( key => key !== 'date' )
|
||||
.map( key => {
|
||||
return {
|
||||
key,
|
||||
value: datapoints[ key ].value,
|
||||
date: d,
|
||||
};
|
||||
} ),
|
||||
};
|
||||
} );
|
||||
|
||||
export const drawLines = ( node, data, params, xOffset ) => {
|
||||
/**
|
||||
* Describes getLine
|
||||
* @param {function} xScale - from `getXLineScale`.
|
||||
* @param {function} yScale - from `getYScale`.
|
||||
* @returns {function} the D3 line function for plotting all category values
|
||||
*/
|
||||
export const getLine = ( xScale, yScale ) =>
|
||||
d3Line()
|
||||
.x( d => xScale( moment( d.date ).toDate() ) )
|
||||
.y( d => yScale( d.value ) );
|
||||
|
||||
/**
|
||||
* Describes `getLineData`
|
||||
* @param {array} data - The chart component's `data` prop.
|
||||
* @param {array} orderedKeys - from `getOrderedKeys`.
|
||||
* @returns {array} an array objects with a category `key` and an array of `values` with `date` and `value` properties
|
||||
*/
|
||||
export const getLineData = ( data, orderedKeys ) =>
|
||||
orderedKeys.map( row => ( {
|
||||
key: row.key,
|
||||
focus: row.focus,
|
||||
visible: row.visible,
|
||||
values: data.map( d => ( {
|
||||
date: d.date,
|
||||
focus: row.focus,
|
||||
label: get( d, [ row.key, 'label' ], '' ),
|
||||
value: get( d, [ row.key, 'value' ], 0 ),
|
||||
visible: row.visible,
|
||||
} ) ),
|
||||
} ) );
|
||||
|
||||
export const drawLines = ( node, data, params, scales, formats, tooltip ) => {
|
||||
const height = scales.yScale.range()[ 0 ];
|
||||
const width = scales.xScale.range()[ 1 ];
|
||||
const line = getLine( scales.xScale, scales.yScale );
|
||||
const lineData = getLineData( data, params.visibleKeys );
|
||||
const series = node
|
||||
.append( 'g' )
|
||||
.attr( 'class', 'lines' )
|
||||
.selectAll( '.line-g' )
|
||||
.data( params.lineData.filter( d => d.visible ).reverse() )
|
||||
.data( lineData.filter( d => d.visible ).reverse() )
|
||||
.enter()
|
||||
.append( 'g' )
|
||||
.attr( 'class', 'line-g' )
|
||||
.attr( 'role', 'region' )
|
||||
.attr( 'aria-label', d => d.key );
|
||||
const dateSpaces = getDateSpaces( data, params.uniqueDates, width, scales.xScale );
|
||||
|
||||
let lineStroke = params.width <= wideBreak || params.uniqueDates.length > 50 ? 2 : 3;
|
||||
lineStroke = params.width <= smallBreak ? 1.25 : lineStroke;
|
||||
const dotRadius = params.width <= wideBreak ? 4 : 6;
|
||||
let lineStroke = width <= wideBreak || params.uniqueDates.length > 50 ? 2 : 3;
|
||||
lineStroke = width <= smallBreak ? 1.25 : lineStroke;
|
||||
const dotRadius = width <= wideBreak ? 4 : 6;
|
||||
|
||||
params.uniqueDates.length > 1 &&
|
||||
series
|
||||
|
@ -48,11 +117,11 @@ export const drawLines = ( node, data, params, xOffset ) => {
|
|||
const opacity = d.focus ? 1 : 0.1;
|
||||
return d.visible ? opacity : 0;
|
||||
} )
|
||||
.attr( 'd', d => params.line( d.values ) );
|
||||
.attr( 'd', d => line( d.values ) );
|
||||
|
||||
const minDataPointSpacing = 36;
|
||||
|
||||
params.width / params.uniqueDates.length > minDataPointSpacing &&
|
||||
width / params.uniqueDates.length > minDataPointSpacing &&
|
||||
series
|
||||
.selectAll( 'circle' )
|
||||
.data( ( d, i ) => d.values.map( row => ( { ...row, i, visible: d.visible, key: d.key } ) ) )
|
||||
|
@ -66,30 +135,25 @@ export const drawLines = ( node, data, params, xOffset ) => {
|
|||
const opacity = d.focus ? 1 : 0.1;
|
||||
return d.visible ? opacity : 0;
|
||||
} )
|
||||
.attr( 'cx', d => params.xLineScale( moment( d.date ).toDate() ) + xOffset )
|
||||
.attr( 'cy', d => params.yScale( d.value ) )
|
||||
.attr( 'cx', d => scales.xScale( moment( d.date ).toDate() ) )
|
||||
.attr( 'cy', d => scales.yScale( d.value ) )
|
||||
.attr( 'tabindex', '0' )
|
||||
.attr( 'aria-label', d => {
|
||||
const label = d.label
|
||||
? d.label
|
||||
: params.tooltipLabelFormat( d.date instanceof Date ? d.date : moment( d.date ).toDate() );
|
||||
return `${ label } ${ params.tooltipValueFormat( d.value ) }`;
|
||||
: tooltip.labelFormat( d.date instanceof Date ? d.date : moment( d.date ).toDate() );
|
||||
return `${ label } ${ tooltip.valueFormat( d.value ) }`;
|
||||
} )
|
||||
.on( 'focus', ( d, i, nodes ) => {
|
||||
const position = calculateTooltipPosition(
|
||||
d3Event.target,
|
||||
node.node(),
|
||||
params.tooltipPosition
|
||||
);
|
||||
handleMouseOverLineChart( d.date, nodes[ i ].parentNode, node, data, params, position );
|
||||
tooltip.show( data.find( e => e.date === d.date ), nodes[ i ].parentNode, d3Event.target );
|
||||
} )
|
||||
.on( 'blur', ( d, i, nodes ) => hideTooltip( nodes[ i ].parentNode, params.tooltip ) );
|
||||
.on( 'blur', () => tooltip.hide() );
|
||||
|
||||
const focus = node
|
||||
.append( 'g' )
|
||||
.attr( 'class', 'focusspaces' )
|
||||
.selectAll( '.focus' )
|
||||
.data( params.dateSpaces )
|
||||
.data( dateSpaces )
|
||||
.enter()
|
||||
.append( 'g' )
|
||||
.attr( 'class', 'focus' );
|
||||
|
@ -101,10 +165,10 @@ export const drawLines = ( node, data, params, xOffset ) => {
|
|||
|
||||
focusGrid
|
||||
.append( 'line' )
|
||||
.attr( 'x1', d => params.xLineScale( moment( d.date ).toDate() ) + xOffset )
|
||||
.attr( 'x1', d => scales.xScale( moment( d.date ).toDate() ) )
|
||||
.attr( 'y1', 0 )
|
||||
.attr( 'x2', d => params.xLineScale( moment( d.date ).toDate() ) + xOffset )
|
||||
.attr( 'y2', params.height );
|
||||
.attr( 'x2', d => scales.xScale( moment( d.date ).toDate() ) )
|
||||
.attr( 'y2', height );
|
||||
|
||||
focusGrid
|
||||
.selectAll( 'circle' )
|
||||
|
@ -115,8 +179,8 @@ export const drawLines = ( node, data, params, xOffset ) => {
|
|||
.attr( 'fill', d => getColor( d.key, params.visibleKeys, params.colorScheme ) )
|
||||
.attr( 'stroke', '#fff' )
|
||||
.attr( 'stroke-width', lineStroke + 2 )
|
||||
.attr( 'cx', d => params.xLineScale( moment( d.date ).toDate() ) + xOffset )
|
||||
.attr( 'cy', d => params.yScale( d.value ) );
|
||||
.attr( 'cx', d => scales.xScale( moment( d.date ).toDate() ) )
|
||||
.attr( 'cy', d => scales.yScale( d.value ) );
|
||||
|
||||
focus
|
||||
.append( 'rect' )
|
||||
|
@ -124,18 +188,12 @@ export const drawLines = ( node, data, params, xOffset ) => {
|
|||
.attr( 'x', d => d.start )
|
||||
.attr( 'y', 0 )
|
||||
.attr( 'width', d => d.width )
|
||||
.attr( 'height', params.height )
|
||||
.attr( 'height', height )
|
||||
.attr( 'opacity', 0 )
|
||||
.on( 'mouseover', ( d, i, nodes ) => {
|
||||
const isTooltipLeftAligned = ( i === 0 || i === params.dateSpaces.length - 1 ) && params.uniqueDates.length > 1;
|
||||
const isTooltipLeftAligned = ( i === 0 || i === dateSpaces.length - 1 ) && params.uniqueDates.length > 1;
|
||||
const elementWidthRatio = isTooltipLeftAligned ? 0 : 0.5;
|
||||
const position = calculateTooltipPosition(
|
||||
d3Event.target,
|
||||
node.node(),
|
||||
params.tooltipPosition,
|
||||
elementWidthRatio
|
||||
);
|
||||
handleMouseOverLineChart( d.date, nodes[ i ].parentNode, node, data, params, position );
|
||||
tooltip.show( data.find( e => e.date === d.date ), d3Event.target, nodes[ i ].parentNode, elementWidthRatio );
|
||||
} )
|
||||
.on( 'mouseout', ( d, i, nodes ) => hideTooltip( nodes[ i ].parentNode, params.tooltip ) );
|
||||
.on( 'mouseout', () => tooltip.hide() );
|
||||
};
|
||||
|
|
|
@ -52,13 +52,26 @@ export const getXLineScale = ( uniqueDates, width ) =>
|
|||
] )
|
||||
.rangeRound( [ 0, width ] );
|
||||
|
||||
const getMaxYValue = data => {
|
||||
let maxYValue = Number.NEGATIVE_INFINITY;
|
||||
data.map( d => {
|
||||
for ( const [ key, item ] of Object.entries( d ) ) {
|
||||
if ( key !== 'date' && Number.isFinite( item.value ) && item.value > maxYValue ) {
|
||||
maxYValue = item.value;
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
return maxYValue;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes and rounds the maximum y value to the nearest thousand, ten-thousand, million etc. In case it is a decimal number, ceils it.
|
||||
* @param {array} lineData - from `getLineData`
|
||||
* @param {array} data - The chart component's `data` prop.
|
||||
* @returns {number} the maximum value in the timeseries multiplied by 4/3
|
||||
*/
|
||||
export const getYMax = lineData => {
|
||||
const maxValue = Math.max( ...lineData.map( d => Math.max( ...d.values.map( date => date.value ) ) ) );
|
||||
export const getYMax = data => {
|
||||
const maxValue = getMaxYValue( data );
|
||||
if ( ! Number.isFinite( maxValue ) || maxValue <= 0 ) {
|
||||
return 0;
|
||||
}
|
||||
|
@ -70,21 +83,10 @@ export const getYMax = lineData => {
|
|||
/**
|
||||
* Describes getYScale
|
||||
* @param {number} height - calculated height of the charting space
|
||||
* @param {number} yMax - from `getYMax`
|
||||
* @param {number} yMax - maximum y value
|
||||
* @returns {function} the D3 linear scale from 0 to the value from `getYMax`
|
||||
*/
|
||||
export const getYScale = ( height, yMax ) =>
|
||||
d3ScaleLinear()
|
||||
.domain( [ 0, yMax === 0 ? 1 : yMax ] )
|
||||
.rangeRound( [ height, 0 ] );
|
||||
|
||||
/**
|
||||
* Describes getyTickOffset
|
||||
* @param {number} height - calculated height of the charting space
|
||||
* @param {number} yMax - from `getYMax`
|
||||
* @returns {function} the D3 linear scale from 0 to the value from `getYMax`, offset by 12 pixels down
|
||||
*/
|
||||
export const getYTickOffset = ( height, yMax ) =>
|
||||
d3ScaleLinear()
|
||||
.domain( [ 0, yMax === 0 ? 1 : yMax ] )
|
||||
.rangeRound( [ height + 12, 12 ] );
|
||||
|
|
|
@ -8,24 +8,14 @@ import { utcParse as d3UTCParse } from 'd3-time-format';
|
|||
* Internal dependencies
|
||||
*/
|
||||
import dummyOrders from './fixtures/dummy-orders';
|
||||
import orderedDates from './fixtures/dummy-ordered-dates';
|
||||
import orderedKeys from './fixtures/dummy-ordered-keys';
|
||||
import {
|
||||
getDateSpaces,
|
||||
getOrderedKeys,
|
||||
getLineData,
|
||||
getUniqueKeys,
|
||||
getUniqueDates,
|
||||
isDataEmpty,
|
||||
} from '../index';
|
||||
import { getXLineScale } from '../scales';
|
||||
|
||||
const parseDate = d3UTCParse( '%Y-%m-%dT%H:%M:%S' );
|
||||
const testUniqueKeys = getUniqueKeys( dummyOrders );
|
||||
const testOrderedKeys = getOrderedKeys( dummyOrders, testUniqueKeys );
|
||||
const testLineData = getLineData( dummyOrders, testOrderedKeys );
|
||||
const testUniqueDates = getUniqueDates( testLineData, parseDate );
|
||||
const testXLineScale = getXLineScale( testUniqueDates, 100 );
|
||||
const testOrderedKeys = getOrderedKeys( dummyOrders );
|
||||
|
||||
describe( 'parseDate', () => {
|
||||
it( 'correctly parse date in the expected format', () => {
|
||||
|
@ -35,67 +25,12 @@ describe( 'parseDate', () => {
|
|||
} );
|
||||
} );
|
||||
|
||||
describe( 'getUniqueKeys', () => {
|
||||
it( 'returns an array of keys excluding date', () => {
|
||||
// sort is a mutating action so we need a copy
|
||||
const testUniqueKeysClone = testUniqueKeys.slice();
|
||||
const sortedAZKeys = orderedKeys.map( d => d.key ).slice();
|
||||
expect( testUniqueKeysClone.sort() ).toEqual( sortedAZKeys.sort() );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getOrderedKeys', () => {
|
||||
it( 'returns an array of keys order by value from largest to smallest', () => {
|
||||
expect( testOrderedKeys ).toEqual( orderedKeys );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getLineData', () => {
|
||||
it( 'returns a sorted array of objects with category key', () => {
|
||||
expect( testLineData ).toBeInstanceOf( Array );
|
||||
expect( testLineData ).toHaveLength( 5 );
|
||||
expect( testLineData.map( d => d.key ) ).toEqual( orderedKeys.map( d => d.key ) );
|
||||
} );
|
||||
|
||||
testLineData.forEach( d => {
|
||||
it( 'ensure a key and that the values property is an array', () => {
|
||||
expect( d ).toHaveProperty( 'key' );
|
||||
expect( d ).toHaveProperty( 'values' );
|
||||
expect( d.values ).toBeInstanceOf( Array );
|
||||
} );
|
||||
|
||||
it( 'ensure all unique dates exist in values array', () => {
|
||||
const rowDates = d.values.map( row => row.date );
|
||||
expect( rowDates ).toEqual( orderedDates );
|
||||
} );
|
||||
|
||||
d.values.forEach( row => {
|
||||
it( 'ensure a date property and that the values property is an array with date (parseable) and value properties', () => {
|
||||
expect( row ).toHaveProperty( 'date' );
|
||||
expect( row ).toHaveProperty( 'value' );
|
||||
expect( parseDate( row.date ) ).not.toBeNull();
|
||||
expect( typeof row.date ).toBe( 'string' );
|
||||
expect( typeof row.value ).toBe( 'number' );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getDateSpaces', () => {
|
||||
it( 'return an array used to space out the mouseover rectangles, used for tooltips', () => {
|
||||
const testDateSpaces = getDateSpaces( dummyOrders, testUniqueDates, 100, testXLineScale );
|
||||
expect( testDateSpaces[ 0 ].date ).toEqual( '2018-05-30T00:00:00' );
|
||||
expect( testDateSpaces[ 0 ].start ).toEqual( 0 );
|
||||
expect( testDateSpaces[ 0 ].width ).toEqual( 10 );
|
||||
expect( testDateSpaces[ 3 ].date ).toEqual( '2018-06-02T00:00:00' );
|
||||
expect( testDateSpaces[ 3 ].start ).toEqual( 50 );
|
||||
expect( testDateSpaces[ 3 ].width ).toEqual( 20 );
|
||||
expect( testDateSpaces[ testDateSpaces.length - 1 ].date ).toEqual( '2018-06-04T00:00:00' );
|
||||
expect( testDateSpaces[ testDateSpaces.length - 1 ].start ).toEqual( 90 );
|
||||
expect( testDateSpaces[ testDateSpaces.length - 1 ].width ).toEqual( 10 );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'isDataEmpty', () => {
|
||||
it( 'should return true when all data values are 0 and no baseValue is provided', () => {
|
||||
const data = [
|
||||
|
|
73
plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/test/line-chart.js
vendored
Normal file
73
plugins/woocommerce-admin/packages/components/src/chart/d3chart/utils/test/line-chart.js
vendored
Normal file
|
@ -0,0 +1,73 @@
|
|||
/** @format */
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { utcParse as d3UTCParse } from 'd3-time-format';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import dummyOrders from './fixtures/dummy-orders';
|
||||
import orderedDates from './fixtures/dummy-ordered-dates';
|
||||
import orderedKeys from './fixtures/dummy-ordered-keys';
|
||||
import {
|
||||
getOrderedKeys,
|
||||
getUniqueDates,
|
||||
} from '../index';
|
||||
import {
|
||||
getDateSpaces,
|
||||
getLineData,
|
||||
} from '../line-chart';
|
||||
import { getXLineScale } from '../scales';
|
||||
|
||||
const parseDate = d3UTCParse( '%Y-%m-%dT%H:%M:%S' );
|
||||
const testOrderedKeys = getOrderedKeys( dummyOrders );
|
||||
const testLineData = getLineData( dummyOrders, testOrderedKeys );
|
||||
const testUniqueDates = getUniqueDates( dummyOrders, '%Y-%m-%dT%H:%M:%S' );
|
||||
const testXLineScale = getXLineScale( testUniqueDates, 100 );
|
||||
|
||||
describe( 'getDateSpaces', () => {
|
||||
it( 'return an array used to space out the mouseover rectangles, used for tooltips', () => {
|
||||
const testDateSpaces = getDateSpaces( dummyOrders, testUniqueDates, 100, testXLineScale );
|
||||
expect( testDateSpaces[ 0 ].date ).toEqual( '2018-05-30T00:00:00' );
|
||||
expect( testDateSpaces[ 0 ].start ).toEqual( 0 );
|
||||
expect( testDateSpaces[ 0 ].width ).toEqual( 10 );
|
||||
expect( testDateSpaces[ 3 ].date ).toEqual( '2018-06-02T00:00:00' );
|
||||
expect( testDateSpaces[ 3 ].start ).toEqual( 50 );
|
||||
expect( testDateSpaces[ 3 ].width ).toEqual( 20 );
|
||||
expect( testDateSpaces[ testDateSpaces.length - 1 ].date ).toEqual( '2018-06-04T00:00:00' );
|
||||
expect( testDateSpaces[ testDateSpaces.length - 1 ].start ).toEqual( 90 );
|
||||
expect( testDateSpaces[ testDateSpaces.length - 1 ].width ).toEqual( 10 );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getLineData', () => {
|
||||
it( 'returns a sorted array of objects with category key', () => {
|
||||
expect( testLineData ).toBeInstanceOf( Array );
|
||||
expect( testLineData ).toHaveLength( 5 );
|
||||
expect( testLineData.map( d => d.key ) ).toEqual( orderedKeys.map( d => d.key ) );
|
||||
} );
|
||||
|
||||
testLineData.forEach( d => {
|
||||
it( 'ensure a key and that the values property is an array', () => {
|
||||
expect( d ).toHaveProperty( 'key' );
|
||||
expect( d ).toHaveProperty( 'values' );
|
||||
expect( d.values ).toBeInstanceOf( Array );
|
||||
} );
|
||||
|
||||
it( 'ensure all unique dates exist in values array', () => {
|
||||
const rowDates = d.values.map( row => row.date );
|
||||
expect( rowDates ).toEqual( orderedDates );
|
||||
} );
|
||||
|
||||
d.values.forEach( row => {
|
||||
it( 'ensure a date property and that the values property is an array with date (parseable) and value properties', () => {
|
||||
expect( row ).toHaveProperty( 'date' );
|
||||
expect( row ).toHaveProperty( 'value' );
|
||||
expect( parseDate( row.date ) ).not.toBeNull();
|
||||
expect( typeof row.date ).toBe( 'string' );
|
||||
expect( typeof row.value ).toBe( 'number' );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
} );
|
|
@ -2,7 +2,6 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { utcParse as d3UTCParse } from 'd3-time-format';
|
||||
import { scaleBand, scaleLinear, scaleTime } from 'd3-scale';
|
||||
|
||||
/**
|
||||
|
@ -11,11 +10,9 @@ import { scaleBand, scaleLinear, scaleTime } from 'd3-scale';
|
|||
import dummyOrders from './fixtures/dummy-orders';
|
||||
import {
|
||||
getOrderedKeys,
|
||||
getLineData,
|
||||
getUniqueKeys,
|
||||
getUniqueDates,
|
||||
} from '../index';
|
||||
import { getXGroupScale, getXScale, getXLineScale, getYMax, getYScale, getYTickOffset } from '../scales';
|
||||
import { getXGroupScale, getXScale, getXLineScale, getYMax, getYScale } from '../scales';
|
||||
|
||||
jest.mock( 'd3-scale', () => ( {
|
||||
...require.requireActual( 'd3-scale' ),
|
||||
|
@ -37,13 +34,10 @@ jest.mock( 'd3-scale', () => ( {
|
|||
} ),
|
||||
} ) );
|
||||
|
||||
const testUniqueKeys = getUniqueKeys( dummyOrders );
|
||||
const testOrderedKeys = getOrderedKeys( dummyOrders, testUniqueKeys );
|
||||
const testLineData = getLineData( dummyOrders, testOrderedKeys );
|
||||
const testOrderedKeys = getOrderedKeys( dummyOrders );
|
||||
|
||||
describe( 'X scales', () => {
|
||||
const parseDate = d3UTCParse( '%Y-%m-%dT%H:%M:%S' );
|
||||
const testUniqueDates = getUniqueDates( testLineData, parseDate );
|
||||
const testUniqueDates = getUniqueDates( dummyOrders, '%Y-%m-%dT%H:%M:%S' );
|
||||
|
||||
describe( 'getXScale', () => {
|
||||
it( 'creates band scale with correct parameters', () => {
|
||||
|
@ -96,7 +90,7 @@ describe( 'X scales', () => {
|
|||
describe( 'Y scales', () => {
|
||||
describe( 'getYMax', () => {
|
||||
it( 'calculate the correct maximum y value', () => {
|
||||
expect( getYMax( testLineData ) ).toEqual( 15000000 );
|
||||
expect( getYMax( dummyOrders ) ).toEqual( 15000000 );
|
||||
} );
|
||||
|
||||
it( 'return 0 if there is no line data', () => {
|
||||
|
@ -120,21 +114,4 @@ describe( 'Y scales', () => {
|
|||
expect( lastArgs[ 0 ] ).toBeLessThan( lastArgs[ 1 ] );
|
||||
} );
|
||||
} );
|
||||
|
||||
describe( 'getYTickOffset', () => {
|
||||
it( 'creates linear scale with correct parameters', () => {
|
||||
getYTickOffset( 100, 15000000 );
|
||||
|
||||
expect( scaleLinear().domain ).toHaveBeenLastCalledWith( [ 0, 15000000 ] );
|
||||
expect( scaleLinear().rangeRound ).toHaveBeenLastCalledWith( [ 112, 12 ] );
|
||||
} );
|
||||
|
||||
it( 'avoids the domain starting and ending at the same point when yMax is 0', () => {
|
||||
getYTickOffset( 100, 0 );
|
||||
|
||||
const args = scaleLinear().domain.mock.calls;
|
||||
const lastArgs = args[ args.length - 1 ][ 0 ];
|
||||
expect( lastArgs[ 0 ] ).toBeLessThan( lastArgs[ 1 ] );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -11,130 +11,137 @@ import moment from 'moment';
|
|||
*/
|
||||
import { getColor } from './color';
|
||||
|
||||
export const hideTooltip = ( parentNode, tooltipNode ) => {
|
||||
d3Select( parentNode )
|
||||
.selectAll( '.barfocus, .focus-grid' )
|
||||
.attr( 'opacity', '0' );
|
||||
d3Select( tooltipNode )
|
||||
.style( 'visibility', 'hidden' );
|
||||
};
|
||||
class ChartTooltip {
|
||||
constructor() {
|
||||
this.ref = null;
|
||||
this.chart = null;
|
||||
this.position = '';
|
||||
this.title = '';
|
||||
this.labelFormat = '';
|
||||
this.valueFormat = '';
|
||||
this.visibleKeys = '';
|
||||
this.colorScheme = null;
|
||||
this.margin = 24;
|
||||
}
|
||||
|
||||
const calculateTooltipXPosition = (
|
||||
calculateXPosition(
|
||||
elementCoords,
|
||||
chartCoords,
|
||||
tooltipSize,
|
||||
tooltipMargin,
|
||||
elementWidthRatio,
|
||||
tooltipPosition
|
||||
) => {
|
||||
) {
|
||||
const tooltipSize = this.ref.getBoundingClientRect();
|
||||
const d3BaseCoords = d3Select( '.d3-base' ).node().getBoundingClientRect();
|
||||
const leftMargin = Math.max( d3BaseCoords.left, chartCoords.left );
|
||||
|
||||
if ( tooltipPosition === 'below' ) {
|
||||
if ( this.position === 'below' ) {
|
||||
return Math.max(
|
||||
tooltipMargin,
|
||||
this.margin,
|
||||
Math.min(
|
||||
elementCoords.left + elementCoords.width * 0.5 - tooltipSize.width / 2 - leftMargin,
|
||||
d3BaseCoords.width - tooltipSize.width - tooltipMargin
|
||||
d3BaseCoords.width - tooltipSize.width - this.margin
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const xPosition =
|
||||
elementCoords.left + elementCoords.width * elementWidthRatio + tooltipMargin - leftMargin;
|
||||
elementCoords.left + elementCoords.width * elementWidthRatio + this.margin - leftMargin;
|
||||
|
||||
if ( xPosition + tooltipSize.width + tooltipMargin > d3BaseCoords.width ) {
|
||||
if ( xPosition + tooltipSize.width + this.margin > d3BaseCoords.width ) {
|
||||
return Math.max(
|
||||
tooltipMargin,
|
||||
this.margin,
|
||||
elementCoords.left +
|
||||
elementCoords.width * ( 1 - elementWidthRatio ) -
|
||||
tooltipSize.width -
|
||||
tooltipMargin -
|
||||
this.margin -
|
||||
leftMargin
|
||||
);
|
||||
}
|
||||
|
||||
return xPosition;
|
||||
};
|
||||
}
|
||||
|
||||
const calculateTooltipYPosition = (
|
||||
calculateYPosition(
|
||||
elementCoords,
|
||||
chartCoords,
|
||||
tooltipSize,
|
||||
tooltipMargin,
|
||||
tooltipPosition
|
||||
) => {
|
||||
if ( tooltipPosition === 'below' ) {
|
||||
) {
|
||||
if ( this.position === 'below' ) {
|
||||
return chartCoords.height;
|
||||
}
|
||||
|
||||
const yPosition = elementCoords.top + tooltipMargin - chartCoords.top;
|
||||
if ( yPosition + tooltipSize.height + tooltipMargin > chartCoords.height ) {
|
||||
return Math.max( 0, elementCoords.top - tooltipSize.height - tooltipMargin - chartCoords.top );
|
||||
const tooltipSize = this.ref.getBoundingClientRect();
|
||||
const yPosition = elementCoords.top + this.margin - chartCoords.top;
|
||||
if ( yPosition + tooltipSize.height + this.margin > chartCoords.height ) {
|
||||
return Math.max( 0, elementCoords.top - tooltipSize.height - this.margin - chartCoords.top );
|
||||
}
|
||||
|
||||
return yPosition;
|
||||
};
|
||||
}
|
||||
|
||||
export const calculateTooltipPosition = ( element, chart, tooltipPosition, elementWidthRatio = 1 ) => {
|
||||
calculatePosition( element, elementWidthRatio = 1 ) {
|
||||
const elementCoords = element.getBoundingClientRect();
|
||||
const chartCoords = chart.getBoundingClientRect();
|
||||
const tooltipSize = d3Select( '.d3-chart__tooltip' )
|
||||
.node()
|
||||
.getBoundingClientRect();
|
||||
const tooltipMargin = 24;
|
||||
const chartCoords = this.chart.getBoundingClientRect();
|
||||
|
||||
if ( tooltipPosition === 'below' ) {
|
||||
if ( this.position === 'below' ) {
|
||||
elementWidthRatio = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
x: calculateTooltipXPosition(
|
||||
x: this.calculateXPosition(
|
||||
elementCoords,
|
||||
chartCoords,
|
||||
tooltipSize,
|
||||
tooltipMargin,
|
||||
elementWidthRatio,
|
||||
tooltipPosition
|
||||
),
|
||||
y: calculateTooltipYPosition(
|
||||
y: this.calculateYPosition(
|
||||
elementCoords,
|
||||
chartCoords,
|
||||
tooltipSize,
|
||||
tooltipMargin,
|
||||
tooltipPosition
|
||||
),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const getTooltipRowLabel = ( d, row, params ) => {
|
||||
hide() {
|
||||
d3Select( this.chart )
|
||||
.selectAll( '.barfocus, .focus-grid' )
|
||||
.attr( 'opacity', '0' );
|
||||
d3Select( this.ref )
|
||||
.style( 'visibility', 'hidden' );
|
||||
}
|
||||
|
||||
getTooltipRowLabel( d, row ) {
|
||||
if ( d[ row.key ].labelDate ) {
|
||||
return params.tooltipLabelFormat( moment( d[ row.key ].labelDate ).toDate() );
|
||||
return this.labelFormat( moment( d[ row.key ].labelDate ).toDate() );
|
||||
}
|
||||
return row.key;
|
||||
};
|
||||
}
|
||||
|
||||
export const showTooltip = ( params, d, position ) => {
|
||||
const keys = params.visibleKeys.map(
|
||||
show( d, triggerElement, parentNode, elementWidthRatio = 1 ) {
|
||||
if ( ! this.visibleKeys.length ) {
|
||||
return;
|
||||
}
|
||||
d3Select( parentNode )
|
||||
.select( '.focus-grid, .barfocus' )
|
||||
.attr( 'opacity', '1' );
|
||||
const position = this.calculatePosition( triggerElement, elementWidthRatio );
|
||||
|
||||
const keys = this.visibleKeys.map(
|
||||
row => `
|
||||
<li class="key-row">
|
||||
<div class="key-container">
|
||||
<span
|
||||
class="key-color"
|
||||
style="background-color:${ getColor( row.key, params.visibleKeys, params.colorScheme ) }">
|
||||
style="background-color: ${ getColor( row.key, this.visibleKeys, this.colorScheme ) }">
|
||||
</span>
|
||||
<span class="key-key">${ getTooltipRowLabel( d, row, params ) }</span>
|
||||
<span class="key-key">${ this.getTooltipRowLabel( d, row ) }</span>
|
||||
</div>
|
||||
<span class="key-value">${ params.tooltipValueFormat( d[ row.key ].value ) }</span>
|
||||
<span class="key-value">${ this.valueFormat( d[ row.key ].value ) }</span>
|
||||
</li>
|
||||
`
|
||||
);
|
||||
|
||||
const tooltipTitle = params.tooltipTitle
|
||||
? params.tooltipTitle
|
||||
: params.tooltipLabelFormat( moment( d.date ).toDate() );
|
||||
const tooltipTitle = this.title
|
||||
? this.title
|
||||
: this.labelFormat( moment( d.date ).toDate() );
|
||||
|
||||
d3Select( params.tooltip )
|
||||
d3Select( this.ref )
|
||||
.style( 'left', position.x + 'px' )
|
||||
.style( 'top', position.y + 'px' )
|
||||
.style( 'visibility', 'visible' ).html( `
|
||||
|
@ -145,4 +152,7 @@ export const showTooltip = ( params, d, position ) => {
|
|||
</ul>
|
||||
</div>
|
||||
` );
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default ChartTooltip;
|
||||
|
|
|
@ -40,8 +40,13 @@ class SearchFilter extends Component {
|
|||
}
|
||||
|
||||
updateLabels( selected ) {
|
||||
const prevIds = this.state.selected.map( item => item.id );
|
||||
const ids = selected.map( item => item.id );
|
||||
|
||||
if ( ! isEqual( ids.sort(), prevIds.sort() ) ) {
|
||||
this.setState( { selected } );
|
||||
}
|
||||
}
|
||||
|
||||
onSearchChange( values ) {
|
||||
this.setState( {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/**
|
||||
* Internal Dependencies
|
||||
*/
|
||||
@import 'segmented-selection/ie.scss';
|
||||
@import 'summary/ie.scss';
|
||||
@import 'table/ie.scss';
|
||||
|
|
|
@ -59,7 +59,7 @@ export class Autocomplete extends Component {
|
|||
this.reset = this.reset.bind( this );
|
||||
this.search = this.search.bind( this );
|
||||
this.handleKeyDown = this.handleKeyDown.bind( this );
|
||||
this.debouncedLoadOptions = debounce( this.loadOptions, 250 );
|
||||
this.debouncedLoadOptions = debounce( this.loadOptions, 400 );
|
||||
|
||||
this.state = this.constructor.getInitialState();
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import Flag from '../../flag';
|
|||
export default {
|
||||
name: 'countries',
|
||||
className: 'woocommerce-search__country-result',
|
||||
isDebounced: true,
|
||||
options() {
|
||||
return wcSettings.dataEndpoints.countries || [];
|
||||
},
|
||||
|
|
|
@ -16,8 +16,6 @@ import { stringifyQuery } from '@woocommerce/navigation';
|
|||
*/
|
||||
import { computeSuggestionMatch } from './utils';
|
||||
|
||||
const getName = customer => [ customer.first_name, customer.last_name ].filter( Boolean ).join( ' ' );
|
||||
|
||||
/**
|
||||
* A customer completer.
|
||||
* See https://github.com/WordPress/gutenberg/tree/master/packages/components/src/autocomplete#the-completer-interface
|
||||
|
@ -40,7 +38,7 @@ export default {
|
|||
},
|
||||
isDebounced: true,
|
||||
getOptionKeywords( customer ) {
|
||||
return [ getName( customer ) ];
|
||||
return [ customer.name ];
|
||||
},
|
||||
getFreeTextOptions( query ) {
|
||||
const label = (
|
||||
|
@ -56,15 +54,15 @@ export default {
|
|||
const nameOption = {
|
||||
key: 'name',
|
||||
label: label,
|
||||
value: { id: query, first_name: query },
|
||||
value: { id: query, name: query },
|
||||
};
|
||||
|
||||
return [ nameOption ];
|
||||
},
|
||||
getOptionLabel( customer, query ) {
|
||||
const match = computeSuggestionMatch( getName( customer ), query ) || {};
|
||||
const match = computeSuggestionMatch( customer.name, query ) || {};
|
||||
return [
|
||||
<span key="name" className="woocommerce-search__result-name" aria-label={ getName( customer ) }>
|
||||
<span key="name" className="woocommerce-search__result-name" aria-label={ customer.name }>
|
||||
{ match.suggestionBeforeMatch }
|
||||
<strong className="components-form-token-field__suggestion-match">
|
||||
{ match.suggestionMatch }
|
||||
|
@ -78,7 +76,7 @@ export default {
|
|||
getOptionCompletion( customer ) {
|
||||
return {
|
||||
id: customer.id,
|
||||
label: getName( customer ),
|
||||
label: customer.name,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
/** @format */
|
||||
|
||||
.woocommerce-segmented-selection__item {
|
||||
display: block;
|
||||
@include set-grid-item-position( 2, 10 );
|
||||
|
||||
&:nth-child(2n) {
|
||||
border-left: 1px solid $core-grey-light-700;
|
||||
border-top: 1px solid $core-grey-light-700;
|
||||
}
|
||||
|
||||
&:nth-child(2n + 1) {
|
||||
border-top: 1px solid $core-grey-light-700;
|
||||
}
|
||||
|
||||
&:nth-child(-n + 2) {
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
|
@ -14,6 +14,24 @@
|
|||
background-color: $core-grey-light-700;
|
||||
}
|
||||
|
||||
.woocommerce-segmented-selection__item {
|
||||
display: block;
|
||||
@include set-grid-item-position( 2, 10 );
|
||||
|
||||
&:nth-child(2n) {
|
||||
border-left: 1px solid $core-grey-light-700;
|
||||
border-top: 1px solid $core-grey-light-700;
|
||||
}
|
||||
|
||||
&:nth-child(2n + 1) {
|
||||
border-top: 1px solid $core-grey-light-700;
|
||||
}
|
||||
|
||||
&:nth-child(-n + 2) {
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-segmented-selection__label {
|
||||
background-color: $core-grey-light-100;
|
||||
padding: $gap-small $gap-small $gap-small $gap-larger;
|
||||
|
|
|
@ -59,9 +59,8 @@ class WC_Tests_API_Reports_Customers_Stats extends WC_REST_Unit_Test_Case {
|
|||
$data = $response->get_data();
|
||||
$properties = $data['schema']['properties'];
|
||||
|
||||
$this->assertCount( 2, $properties );
|
||||
$this->assertCount( 1, $properties );
|
||||
$this->assertArrayHasKey( 'totals', $properties );
|
||||
$this->assertArrayHasKey( 'intervals', $properties );
|
||||
$this->assertCount( 4, $properties['totals']['properties'] );
|
||||
$this->assertArrayHasKey( 'customers_count', $properties['totals']['properties'] );
|
||||
$this->assertArrayHasKey( 'avg_orders_count', $properties['totals']['properties'] );
|
||||
|
@ -142,7 +141,7 @@ class WC_Tests_API_Reports_Customers_Stats extends WC_REST_Unit_Test_Case {
|
|||
// Test name parameter (case with no matches).
|
||||
$request->set_query_params(
|
||||
array(
|
||||
'name' => 'Nota Customername',
|
||||
'search' => 'Nota Customername',
|
||||
)
|
||||
);
|
||||
$response = $this->server->dispatch( $request );
|
||||
|
@ -157,7 +156,7 @@ class WC_Tests_API_Reports_Customers_Stats extends WC_REST_Unit_Test_Case {
|
|||
// Test name and last_order parameters.
|
||||
$request->set_query_params(
|
||||
array(
|
||||
'name' => 'Jeff',
|
||||
'search' => 'Jeff',
|
||||
'last_order_after' => date( 'Y-m-d' ) . 'T00:00:00Z',
|
||||
)
|
||||
);
|
||||
|
|
|
@ -52,7 +52,7 @@ class WC_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case {
|
|||
* @param array $schema Item to check schema.
|
||||
*/
|
||||
public function assert_report_item_schema( $schema ) {
|
||||
$this->assertArrayHasKey( 'customer_id', $schema );
|
||||
$this->assertArrayHasKey( 'id', $schema );
|
||||
$this->assertArrayHasKey( 'user_id', $schema );
|
||||
$this->assertArrayHasKey( 'name', $schema );
|
||||
$this->assertArrayHasKey( 'username', $schema );
|
||||
|
@ -163,7 +163,7 @@ class WC_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case {
|
|||
// Test name parameter (case with no matches).
|
||||
$request->set_query_params(
|
||||
array(
|
||||
'name' => 'Nota Customername',
|
||||
'search' => 'Nota Customername',
|
||||
)
|
||||
);
|
||||
$response = $this->server->dispatch( $request );
|
||||
|
@ -175,7 +175,7 @@ class WC_Tests_API_Reports_Customers extends WC_REST_Unit_Test_Case {
|
|||
// Test name and last_order parameters.
|
||||
$request->set_query_params(
|
||||
array(
|
||||
'name' => 'Justin',
|
||||
'search' => 'Justin',
|
||||
'last_order_after' => date( 'Y-m-d' ) . 'T00:00:00Z',
|
||||
)
|
||||
);
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
/**
|
||||
* Reports Stock Stats REST API Test
|
||||
*
|
||||
* @package WooCommerce\Tests\API
|
||||
*/
|
||||
|
||||
/**
|
||||
* Class WC_Tests_API_Reports_Stock_Stats
|
||||
*/
|
||||
class WC_Tests_API_Reports_Stock_Stats extends WC_REST_Unit_Test_Case {
|
||||
|
||||
/**
|
||||
* Endpoints.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $endpoint = '/wc/v4/reports/stock/stats';
|
||||
|
||||
/**
|
||||
* Setup test reports stock data.
|
||||
*/
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->user = $this->factory->user->create(
|
||||
array(
|
||||
'role' => 'administrator',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test route registration.
|
||||
*/
|
||||
public function test_register_routes() {
|
||||
$routes = $this->server->get_routes();
|
||||
|
||||
$this->assertArrayHasKey( $this->endpoint, $routes );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getting reports.
|
||||
*/
|
||||
public function test_get_reports() {
|
||||
wp_set_current_user( $this->user );
|
||||
WC_Helper_Reports::reset_stats_dbs();
|
||||
|
||||
$number_of_low_stock = 3;
|
||||
for ( $i = 1; $i <= $number_of_low_stock; $i++ ) {
|
||||
$low_stock = new WC_Product_Simple();
|
||||
$low_stock->set_name( "Test low stock {$i}" );
|
||||
$low_stock->set_regular_price( 5 );
|
||||
$low_stock->set_manage_stock( true );
|
||||
$low_stock->set_stock_quantity( 1 );
|
||||
$low_stock->set_stock_status( 'instock' );
|
||||
$low_stock->save();
|
||||
}
|
||||
|
||||
$number_of_out_of_stock = 6;
|
||||
for ( $i = 1; $i <= $number_of_out_of_stock; $i++ ) {
|
||||
$out_of_stock = new WC_Product_Simple();
|
||||
$out_of_stock->set_name( "Test out of stock {$i}" );
|
||||
$out_of_stock->set_regular_price( 5 );
|
||||
$out_of_stock->set_stock_status( 'outofstock' );
|
||||
$out_of_stock->save();
|
||||
}
|
||||
|
||||
$number_of_in_stock = 10;
|
||||
for ( $i = 1; $i <= $number_of_in_stock; $i++ ) {
|
||||
$in_stock = new WC_Product_Simple();
|
||||
$in_stock->set_name( "Test in stock {$i}" );
|
||||
$in_stock->set_regular_price( 25 );
|
||||
$in_stock->save();
|
||||
}
|
||||
|
||||
$request = new WP_REST_Request( 'GET', $this->endpoint );
|
||||
$response = $this->server->dispatch( $request );
|
||||
$reports = $response->get_data();
|
||||
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
|
||||
$this->assertArrayHasKey( 'totals', $reports );
|
||||
$this->assertEquals( 19, $reports['totals']['products'] );
|
||||
$this->assertEquals( 6, $reports['totals']['outofstock'] );
|
||||
$this->assertEquals( 0, $reports['totals']['onbackorder'] );
|
||||
$this->assertEquals( 3, $reports['totals']['lowstock'] );
|
||||
$this->assertEquals( 13, $reports['totals']['instock'] );
|
||||
|
||||
// Test backorder and cache update.
|
||||
$backorder_stock = new WC_Product_Simple();
|
||||
$backorder_stock->set_name( 'Test backorder' );
|
||||
$backorder_stock->set_regular_price( 5 );
|
||||
$backorder_stock->set_stock_status( 'onbackorder' );
|
||||
$backorder_stock->save();
|
||||
|
||||
$request = new WP_REST_Request( 'GET', $this->endpoint );
|
||||
$response = $this->server->dispatch( $request );
|
||||
$reports = $response->get_data();
|
||||
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
|
||||
$this->assertEquals( 20, $reports['totals']['products'] );
|
||||
$this->assertEquals( 6, $reports['totals']['outofstock'] );
|
||||
$this->assertEquals( 1, $reports['totals']['onbackorder'] );
|
||||
$this->assertEquals( 3, $reports['totals']['lowstock'] );
|
||||
$this->assertEquals( 13, $reports['totals']['instock'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test getting reports without valid permissions.
|
||||
*/
|
||||
public function test_get_reports_without_permission() {
|
||||
wp_set_current_user( 0 );
|
||||
$response = $this->server->dispatch( new WP_REST_Request( 'GET', $this->endpoint ) );
|
||||
$this->assertEquals( 401, $response->get_status() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test reports schema.
|
||||
*/
|
||||
public function test_reports_schema() {
|
||||
wp_set_current_user( $this->user );
|
||||
|
||||
$request = new WP_REST_Request( 'OPTIONS', $this->endpoint );
|
||||
$response = $this->server->dispatch( $request );
|
||||
$data = $response->get_data();
|
||||
$properties = $data['schema']['properties'];
|
||||
|
||||
$this->assertCount( 1, $properties );
|
||||
$this->assertArrayHasKey( 'totals', $properties );
|
||||
$this->assertCount( 5, $properties['totals']['properties'] );
|
||||
$this->assertArrayHasKey( 'products', $properties['totals']['properties'] );
|
||||
$this->assertArrayHasKey( 'outofstock', $properties['totals']['properties'] );
|
||||
$this->assertArrayHasKey( 'onbackorder', $properties['totals']['properties'] );
|
||||
$this->assertArrayHasKey( 'lowstock', $properties['totals']['properties'] );
|
||||
$this->assertArrayHasKey( 'instock', $properties['totals']['properties'] );
|
||||
}
|
||||
}
|
|
@ -178,9 +178,6 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
$order->set_shipping_total( 0 );
|
||||
$order->set_cart_tax( 0 );
|
||||
$order->save();
|
||||
|
||||
// Wait one second to avoid potentially ambiguous new/returning customer.
|
||||
sleep( 1 ); // @todo Remove this after p90Yrv-XN-p2 is resolved.
|
||||
}
|
||||
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
|
@ -990,8 +987,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => 2,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'products' => 2,
|
||||
'segments' => array(),
|
||||
// product 3 and product 4 (that is sometimes included in the orders with product 3).
|
||||
|
@ -1014,8 +1011,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => 2,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'segments' => array(),
|
||||
),
|
||||
),
|
||||
|
@ -1064,8 +1061,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => 2,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'products' => 3,
|
||||
'segments' => array(),
|
||||
),
|
||||
|
@ -1087,8 +1084,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => 2,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'segments' => array(),
|
||||
),
|
||||
),
|
||||
|
@ -1136,8 +1133,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => 2,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'products' => 2,
|
||||
'segments' => array(),
|
||||
),
|
||||
|
@ -1159,8 +1156,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => 2,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'segments' => array(),
|
||||
),
|
||||
),
|
||||
|
@ -1211,8 +1208,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => 2,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'products' => 2,
|
||||
'segments' => array(),
|
||||
),
|
||||
|
@ -1234,8 +1231,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => 2,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'segments' => array(),
|
||||
),
|
||||
),
|
||||
|
@ -1288,8 +1285,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => 2,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'products' => 4,
|
||||
'segments' => array(),
|
||||
),
|
||||
|
@ -1311,8 +1308,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => 2,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'segments' => array(),
|
||||
),
|
||||
),
|
||||
|
@ -1362,8 +1359,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => 2,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'products' => 4,
|
||||
'segments' => array(),
|
||||
),
|
||||
|
@ -1385,8 +1382,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => 2,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'segments' => array(),
|
||||
),
|
||||
),
|
||||
|
@ -1591,8 +1588,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => $new_customers,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'products' => 4,
|
||||
'segments' => array(),
|
||||
),
|
||||
|
@ -1614,8 +1611,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => $new_customers,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'segments' => array(),
|
||||
),
|
||||
),
|
||||
|
@ -1635,18 +1632,11 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'customer' => 'new',
|
||||
);
|
||||
|
||||
$orders_count = 144;
|
||||
$num_items_sold = $orders_count / 2 * $qty_per_product
|
||||
+ $orders_count / 2 * $qty_per_product * 2;
|
||||
$coupons = $orders_count;
|
||||
$orders_count = 2;
|
||||
$num_items_sold = $orders_count * $qty_per_product;
|
||||
$coupons = 0;
|
||||
$shipping = $orders_count * 10;
|
||||
$net_revenue = $product_1_price * $qty_per_product * ( $orders_count / 6 )
|
||||
+ $product_2_price * $qty_per_product * ( $orders_count / 6 )
|
||||
+ $product_3_price * $qty_per_product * ( $orders_count / 6 )
|
||||
+ ( $product_1_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
|
||||
+ ( $product_2_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
|
||||
+ ( $product_3_price + $product_4_price ) * $qty_per_product * ( $orders_count / 6 )
|
||||
- $coupons;
|
||||
$net_revenue = $product_1_price * $qty_per_product * $orders_count;
|
||||
$gross_revenue = $net_revenue + $shipping;
|
||||
|
||||
$expected_stats = array(
|
||||
|
@ -1663,7 +1653,7 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => $new_customers,
|
||||
'products' => 4,
|
||||
'products' => 1,
|
||||
'segments' => array(),
|
||||
),
|
||||
'intervals' => array(
|
||||
|
@ -1697,15 +1687,6 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
$this->assertEquals( $expected_stats, json_decode( json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . print_r( $query_args, true ) . "; query: {$wpdb->last_query}" );
|
||||
|
||||
// ** Customer returning
|
||||
$returning_order = WC_Helper_Order::create_order( $customer_1->get_id(), $product );
|
||||
$returning_order->set_status( 'completed' );
|
||||
$returning_order->set_shipping_total( 10 );
|
||||
$returning_order->set_total( 110 ); // $25x4 products + $10 shipping.
|
||||
$returning_order->set_date_created( $order_1_time + 1 ); // This is guaranteed to belong to the same hour by the adjustment to $order_1_time.
|
||||
$returning_order->save();
|
||||
|
||||
WC_Helper_Queue::run_all_pending();
|
||||
|
||||
$query_args = array(
|
||||
'after' => $current_hour_start->format( WC_Admin_Reports_Interval::$sql_datetime_format ), // I don't think this makes sense.... date( 'Y-m-d H:i:s', $orders[0]->get_date_created()->getOffsetTimestamp() + 1 ), // Date after initial order to get a returning customer.
|
||||
'before' => $current_hour_end->format( WC_Admin_Reports_Interval::$sql_datetime_format ),
|
||||
|
@ -1713,15 +1694,23 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'customer' => 'returning',
|
||||
);
|
||||
|
||||
$order_permutations = 72;
|
||||
$total_orders_count = 144;
|
||||
$returning_orders_count = 2;
|
||||
$order_w_coupon_1_perms = 24;
|
||||
$order_w_coupon_2_perms = 24;
|
||||
|
||||
$orders_count = 1;
|
||||
$num_items_sold = 4;
|
||||
$coupons = 0;
|
||||
$orders_count = $total_orders_count - $returning_orders_count;
|
||||
$num_items_sold = $total_orders_count * 6 - ( $returning_orders_count * 4 );
|
||||
$coupons = count( $this_['hour'] ) * ( $order_w_coupon_1_perms * $coupon_1_amount + $order_w_coupon_2_perms * $coupon_2_amount );
|
||||
$shipping = $orders_count * 10;
|
||||
$net_revenue = 100;
|
||||
$net_revenue = $product_1_price * $qty_per_product * ( $total_orders_count / 6 )
|
||||
+ $product_2_price * $qty_per_product * ( $total_orders_count / 6 )
|
||||
+ $product_3_price * $qty_per_product * ( $total_orders_count / 6 )
|
||||
+ ( $product_1_price + $product_4_price ) * $qty_per_product * ( $total_orders_count / 6 )
|
||||
+ ( $product_2_price + $product_4_price ) * $qty_per_product * ( $total_orders_count / 6 )
|
||||
+ ( $product_3_price + $product_4_price ) * $qty_per_product * ( $total_orders_count / 6 )
|
||||
- $product_1_price * $qty_per_product * $returning_orders_count
|
||||
- $coupons;
|
||||
$gross_revenue = $net_revenue + $shipping;
|
||||
|
||||
$expected_stats = array(
|
||||
|
@ -1734,11 +1723,11 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'taxes' => 0,
|
||||
'shipping' => $shipping,
|
||||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold,
|
||||
'avg_items_per_order' => round( $num_items_sold / $orders_count, 4 ),
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 1,
|
||||
'num_returning_customers' => $returning_orders_count,
|
||||
'num_new_customers' => 0,
|
||||
'products' => 1,
|
||||
'products' => 4,
|
||||
'segments' => array(),
|
||||
),
|
||||
'intervals' => array(
|
||||
|
@ -1757,9 +1746,9 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'taxes' => 0,
|
||||
'shipping' => $shipping,
|
||||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold,
|
||||
'avg_items_per_order' => round( $num_items_sold / $orders_count, 4 ),
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 1,
|
||||
'num_returning_customers' => $returning_orders_count,
|
||||
'num_new_customers' => 0,
|
||||
'segments' => array(),
|
||||
),
|
||||
|
@ -1771,7 +1760,6 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
);
|
||||
|
||||
$this->assertEquals( $expected_stats, json_decode( json_encode( $data_store->get_data( $query_args ) ), true ), 'Query args: ' . print_r( $query_args, true ) . "; query: {$wpdb->last_query};" );
|
||||
wp_delete_post( $returning_order->get_id(), true );
|
||||
|
||||
// Combinations: match all
|
||||
// status_is + product_includes.
|
||||
|
@ -1891,8 +1879,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => $new_customers,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'products' => 4,
|
||||
'segments' => array(),
|
||||
),
|
||||
|
@ -1914,8 +1902,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => $new_customers,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'segments' => array(),
|
||||
),
|
||||
),
|
||||
|
@ -1965,8 +1953,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => $new_customers,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'products' => 2,
|
||||
'segments' => array(),
|
||||
),
|
||||
|
@ -1988,8 +1976,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => $new_customers,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'segments' => array(),
|
||||
),
|
||||
),
|
||||
|
@ -2042,8 +2030,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => $new_customers,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'products' => 2,
|
||||
'segments' => array(),
|
||||
),
|
||||
|
@ -2065,8 +2053,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => $new_customers,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'segments' => array(),
|
||||
),
|
||||
),
|
||||
|
@ -2123,8 +2111,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => $new_customers,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'products' => 2,
|
||||
'segments' => array(),
|
||||
),
|
||||
|
@ -2146,8 +2134,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => $new_customers,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'segments' => array(),
|
||||
),
|
||||
),
|
||||
|
@ -2289,8 +2277,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => $new_customers,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'products' => 2,
|
||||
'segments' => array(),
|
||||
),
|
||||
|
@ -2312,8 +2300,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => $new_customers,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'segments' => array(),
|
||||
),
|
||||
),
|
||||
|
@ -2377,8 +2365,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => $new_customers,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'products' => 2,
|
||||
'segments' => array(),
|
||||
),
|
||||
|
@ -2400,8 +2388,8 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
'net_revenue' => $net_revenue,
|
||||
'avg_items_per_order' => $num_items_sold / $orders_count,
|
||||
'avg_order_value' => $net_revenue / $orders_count,
|
||||
'num_returning_customers' => 0,
|
||||
'num_new_customers' => $new_customers,
|
||||
'num_returning_customers' => 2,
|
||||
'num_new_customers' => 0,
|
||||
'segments' => array(),
|
||||
),
|
||||
),
|
||||
|
@ -4732,7 +4720,6 @@ class WC_Tests_Reports_Orders_Stats extends WC_Unit_Test_Case {
|
|||
) as $order_time
|
||||
) {
|
||||
// Order with 1 product.
|
||||
sleep( 1 ); // @todo Remove this after p90Yrv-XN-p2 is resolved.
|
||||
$order = WC_Helper_Order::create_order( $customer->get_id(), $product );
|
||||
$order->set_date_created( $order_time );
|
||||
$order->set_status( $order_status );
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* Author URI: https://woocommerce.com/
|
||||
* Text Domain: wc-admin
|
||||
* Domain Path: /languages
|
||||
* Version: 0.6.0
|
||||
* Version: 0.7.0
|
||||
*
|
||||
* @package WC_Admin
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue