An NDSolve solution, because I like NDSolve and it's just hanging around on my laptop. (Note: Function to be plotted must be smooth.)
calcPlot[x^Sin[x],
{x, 0, 20},
MeshStyle -> {PointSize@Medium},
PlotLegends -> Placed[calcPlot["Legend"], {0.25, 0.75}]]

The solutions are cached:
calcPlot["CPs"]
(*
<|"Min" -> {{4.84255, 0.209274}, {0.352214, 0.697671}, {11.0333,
0.0907897}, {17.299, 0.0578358}},
"Max" -> {{7.91498, 7.88459}, {2.12762, 1.89828}, {14.1638,
14.1505}, {20.4366, 20.4284}},
"IP" -> {{8.57615, 5.01558}, {7.25862, 5.16106}, {2.9161,
1.27035}, {1.39529, 1.38816}, {13.5721, 9.048}, {14.7568,
8.94732}, {19.8785, 12.9545}, {20.9952, 12.8718}}|>
*)
Code dump:
Options through the Method option of ListLinePlot:
"CPs" (which solutions to collect & show, among {"Root", "Min", "Max", "IP"}).
"CPStyles" (colors for the points).
"NDSolveOptions" (options passed to NDSolve).
Also if PlotLegends contains calcPlot["Legend"], it will be replaced by a PointLegend[].
calcPlot // ClearAll;
calcPlot["CPs"] = <||>; (* last result cache *)
calcPlot[f_, {x_, a_, b_}, opts : OptionsPattern[ListLinePlot]] :=
Module[{y, x0, meth, cps, styles, ndopts, legend},
(*
starting point with a 'random' offset:
events are not detected on the first step
so we try to avoid symmetry
*)
x0 = (a (1 + Sqrt@$MachineEpsilon) +
b (1 - Sqrt@$MachineEpsilon))/2;
(* Method options *)
meth = OptionValue[Method];
If[! OptionQ[meth], meth = {}];
cps = Replace["CPs" /. meth,
{"CPs" -> {"Min", "Max", "IP"},
s_String :> {s},
All -> {"Root", "Min", "Max", "IP"},
Except[{___String}] :> {}}];
styles =
Replace[
"CPStyles" /.
meth, {"CPStyles" -> {"Root" -> Darker@Yellow, "Min" -> Magenta,
"Max" -> Purple, "CP" -> Red, "IP" -> Darker@Green, _ -> Black},
s : {___} :> Append[s, _ -> Black],
s_Rule :> {s, _ -> Black}
}];
ndopts = Replace["NDSolveOptions" /. meth, {"NDSolveOptions" -> {}}];
legend = OptionValue[PlotLegends] /. Automatic -> calcPlot["Legend"];
If[! FreeQ[legend, calcPlot["Legend"]],
With[{leg = legend},
legend := leg /. calcPlot["Legend"] :>
PointLegend[
Replace[Keys@calcPlot["CPs"], styles, 1],
Keys@calcPlot["CPs"],
Joined -> {False}] (* override ListLinePlot *)
]
];
Reap[ (* calculate graph & critical points *)
NDSolveValue[{
y'''[x] == D[f, {x, 3}],
y[x0] == f /. x -> x0,
y'[x0] == D[f, x] /. x -> x0,
y''[x0] == D[f, {x, 2}] /. x -> x0,
WhenEvent[y[x] == 0, {Sow[{x, y[x]}, "Root"]}],
WhenEvent[(x - x0) y'[x] > 0, {Sow[{x, y[x]}, {"CP", "Min"}]}],
WhenEvent[(x - x0) y'[x] < 0, {Sow[{x, y[x]}, {"CP", "Max"}]}],
WhenEvent[y''[x] == 0, {Sow[{x, y[x]}, "IP"]}]}, y,
(* offset to avoid singularities at end points *)
{x,
a + (b - a) $MachineEpsilon, b - (b - a) $MachineEpsilon},
ndopts,
MaxStepFraction -> 1/50, (* since we're plotting *)
(* try not to skip an event near the start: *)
StartingStepSize -> (b - a) Sqrt@$MachineEpsilon/10],
cps,
Rule] // (* process results: *)
ListLinePlot[First@#,
PlotLegends :> legend,
opts,
Epilog -> {
Replace[
OptionValue@MeshStyle, {Automatic -> {},
o_List :> Directive @@ o}],
calcPlot["CPs"] = Association@*Join @@ Last@#;
KeyValueMap[
{Replace[#1, styles],
Point /@ #2} &,
calcPlot["CPs"]]},
PlotRange -> All] &
];