• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Yoast / wordpress-seo / 52e9cec460a22744de3786e650690f18927b01c1

26 Feb 2025 04:38PM UTC coverage: 57.765% (-0.1%) from 57.907%
52e9cec460a22744de3786e650690f18927b01c1

Pull #22073

github

web-flow
Merge cb8737086 into 6b3a2490c
Pull Request #22073: Add Organic Sessions widget

7852 of 13969 branches covered (56.21%)

Branch coverage included in aggregate %.

17 of 68 new or added lines in 6 files covered. (25.0%)

10 existing lines in 3 files now uncovered.

13713 of 23363 relevant lines covered (58.7%)

98475.41 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

25.0
/packages/js/src/dashboard/widgets/organic-sessions/daily.js
1
import { useCallback, useMemo } from "@wordpress/element";
2
import { __ } from "@wordpress/i18n";
3
import { SkeletonLoader } from "@yoast/ui-library";
4
import { Chart, Filler } from "chart.js";
5
import { Line } from "react-chartjs-2";
6
import { useRemoteData } from "../../services/use-remote-data";
7
import { ErrorAlert } from "../../components/error-alert";
8

9
/**
10
 * @type {import("../services/data-provider")} DataProvider
11
 * @type {import("../services/remote-data-provider")} RemoteDataProvider
12
 * @type {import("../services/data-formatter")} DataFormatter
13
 */
14

15
/**
16
 * @typedef {Object} OrganicSessionsDailyData The raw organic sessions daily data.
17
 * @property {string} date The date.
18
 * @property {number} sessions The number of sessions.
19
 */
20

21
/**
22
 * @typedef {Object} ChartData
23
 * @property {string[]} labels The labels.
24
 * @property {{data: OrganicSessionsDailyData, fill: string}[]} datasets The datasets.
25
 */
26

27
// Register the Filler plugin to fill the area under the line in the chart.
28
Chart.register( Filler );
2✔
29

30
const COLORS = {
2✔
31
        primary500: "rgba(166, 30, 105, 1)",
32
        primary500Alpha20: "rgba(166, 30, 105, 0.2)",
33
        primary500Alpha0: "rgba(166, 30, 105, 0)",
34
        slate500: "oklch(0.554 0.046 257.417)",
35
        slate300: "oklch(0.869 0.022 252.894)",
36
        slate200: "oklch(0.929 0.013 255.508)",
37
        transparent: "transparent",
38
};
39

40
// Using a memory canvas context. This prevents needing a React ref and creating these variables on-the-fly.
41
// Using y 471 because that is the height of the chart.
42
const CHART_GRADIENT = document.createElement( "canvas" )?.getContext( "2d" )?.createLinearGradient( 0, 0, 0, 471 );
2✔
43
CHART_GRADIENT?.addColorStop( 0, COLORS.primary500Alpha20 );
2✔
44
CHART_GRADIENT?.addColorStop( 1, COLORS.primary500Alpha0 );
2✔
45

46
const CHART_OPTIONS = {
2✔
47
        parsing: {
48
                xAxisKey: "date",
49
                yAxisKey: "sessions",
50
        },
51
        elements: {
52
                point: {
53
                        radius: 5,
54
                        borderWidth: 2,
55
                        borderColor: "white",
56
                        backgroundColor: COLORS.primary500,
57
                },
58
                line: {
59
                        tension: 0.3,
60
                        borderWidth: 3,
61
                        borderColor: COLORS.primary500,
62
                        backgroundColor: CHART_GRADIENT || COLORS.transparent,
2✔
63
                },
64
        },
65
        scales: {
66
                x: {
67
                        grid: {
68
                                color: COLORS.slate300,
69
                                drawTicks: false,
70
                        },
71
                        ticks: {
72
                                font: {
73
                                        size: 12,
74
                                        weight: 400,
75
                                },
76
                                padding: 12,
77
                                maxRotation: 0,
78
                                // Limit the number of ticks to 14, which is half of the 28 days.
79
                                maxTicksLimit: 14,
80
                        },
81
                },
82
                y: {
83
                        grid: {
84
                                // Only show the grid line for whole numbers.
NEW
85
                                color: ( context ) => context.tick.value % 1 ? COLORS.transparent : COLORS.slate200,
×
86
                                drawTicks: false,
87
                        },
88
                        ticks: {
89
                                color: COLORS.slate500,
90
                                font: {
91
                                        size: 14,
92
                                        weight: 400,
93
                                },
94
                                padding: 20,
95
                                // Set the offset for y-axis ticks
96
                                callback: function( value ) {
97
                                        // Only show the label for whole numbers.
NEW
98
                                        const number = value % 1 ? "" : this.getLabelForValue( value );
×
NEW
99
                                        if ( number === "0" ) {
×
NEW
100
                                                return number;
×
101
                                        }
NEW
102
                                        return number ? `${number}k` : "";
×
103
                                },
104
                        },
105
                },
106
        },
107
        responsive: true,
108
        maintainAspectRatio: false,
109
        plugins: {
110
                legend: false,
111
                tooltip: {
112
                        displayColors: false,
113
                        callbacks: {
NEW
114
                                title: () => "",
×
NEW
115
                                label: context => `${ context.label }: ${ context?.formattedValue }`,
×
116
                        },
117
                },
118
        },
119
};
120

121
/**
122
 * @param {OrganicSessionsDailyData[]} organicSessions The organic sessions data.
123
 * @returns {ChartData} The chart data.
124
 */
125
const transformOrganicSessionsDataToChartData = ( organicSessions ) => ( {
2✔
NEW
126
        labels: organicSessions.map( ( { date } ) => date ),
×
127
        datasets: [ { fill: "origin", data: organicSessions } ],
128
} );
129

130
/**
131
 * @param {DataFormatter} dataFormatter The data formatter.
132
 * @returns {function(?OrganicSessionsDailyData[]): OrganicSessionsDailyData[]} Function to format the organic sessions daily data.
133
 */
134
export const createOrganicSessionsDailyFormatter = ( dataFormatter ) => ( data = [] ) => data.map( ( item ) => ( {
2!
135
        date: dataFormatter.format( item.date, "date", { widget: "organicSessions" } ),
136
        sessions: dataFormatter.format( item.sessions, "sessions", { widget: "organicSessions", type: "daily" } ),
137
} ) );
138

139
/**
140
 * @param {ChartData} data The chart data.
141
 * @returns {JSX.Element} The chart.
142
 */
143
const OrganicSessionsChart = ( { data } ) => (
2✔
NEW
144
        <>
×
145
                <div className="yst-w-full yst-h-[226px]">
146
                        <Line
147
                                aria-hidden={ true }
148
                                options={ CHART_OPTIONS }
149
                                data={ data }
150
                                className="-yst-ms-5"
151
                        />
152
                </div>
153
                <table className="yst-sr-only">
154
                        <caption>{ __( "Organic sessions chart", "wordpress-seo" ) }</caption>
155
                        <thead>
156
                                <tr>
157
                                        { data.labels.map( ( label ) => (
NEW
158
                                                <th key={ label }>{ label }</th>
×
159
                                        ) ) }
160
                                </tr>
161
                        </thead>
162
                        <tbody>
163
                                <tr>
164
                                        { data.datasets[ 0 ].data.map( ( { date, sessions } ) => (
NEW
165
                                                <td key={ date }>{ sessions }</td>
×
166
                                        ) ) }
167
                                </tr>
168
                        </tbody>
169
                </table>
170
        </>
171
);
172

173
/**
174
 * @param {DataProvider} dataProvider The data provider.
175
 * @param {RemoteDataProvider} remoteDataProvider The remote data provider.
176
 * @param {DataFormatter} dataFormatter The data formatter.
177
 * @returns {{data: *, error: Error, isPending: boolean}} The remote data info.
178
 */
179
export const useOrganicSessionsDaily = ( dataProvider, remoteDataProvider, dataFormatter ) => {
2✔
180
        /**
181
         * @param {RequestInit} options The options.
182
         * @returns {Promise<OrganicSessionsDailyData[]|Error>} The promise of OrganicSessionsData or an Error.
183
         */
NEW
184
        const getOrganicSessionsDaily = useCallback( ( options ) => {
×
NEW
185
                return remoteDataProvider.fetchJson(
×
186
                        dataProvider.getEndpoint( "timeBasedSeoMetrics" ),
187
                        { options: { widget: "οrganicSessionsDaily" } },
188
                        options );
189
        }, [ dataProvider ] );
190

191
        /**
192
         * @type {function(?OrganicSessionsDailyData[]): ChartData} Function to format the organic sessions data into chart data.
193
         */
NEW
194
        const formatOrganicSessionsDailyToChartData = useMemo( () => ( rawData = [] ) => {
×
NEW
195
                return transformOrganicSessionsDataToChartData( createOrganicSessionsDailyFormatter( dataFormatter )( rawData ) );
×
196
        }, [ dataFormatter ] );
197

NEW
198
        return useRemoteData( getOrganicSessionsDaily, formatOrganicSessionsDailyToChartData );
×
199
};
200

201
/**
202
 * @param {ChartData} data The chart data.
203
 * @param {boolean} isPending Whether the data is pending.
204
 * @param {?Error} [error] The error.
205
 * @param {string} supportLink The support link.
206
 * @returns {JSX.Element} The element.
207
 */
208
export const OrganicSessionsDaily = ( { data, isPending, error, supportLink } ) => {
2✔
NEW
209
        if ( isPending ) {
×
NEW
210
                return (
×
211
                        <SkeletonLoader className="yst-w-full yst-h-[226px]" />
212
                );
213
        }
NEW
214
        if ( error ) {
×
NEW
215
                return (
×
216
                        <ErrorAlert error={ error } className="yst-mt-4" supportLink={ supportLink } />
217
                );
218
        }
NEW
219
        if ( data.labels.length === 0 ) {
×
NEW
220
                return (
×
221
                        <p>
222
                                { __( "No data to display: Your site hasn't received any visitors yet.", "wordpress-seo" ) }
223
                        </p>
224
                );
225
        }
226

NEW
227
        return (
×
228
                <OrganicSessionsChart data={ data } />
229
        );
230
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc