I needed a trustworthy history of mining difficulty changes and also landed on the blockchain.info chart that clearly has incorrect (averaged) data. So I wrote this script that uses the Esplora API to search for difficulty changes. The logic could be easily ported to run against your own Esplora instance or your own Bitcoin node's RPC API.
Script is here: https://github.com/jlopp/bitcoin-utils/blob/master/generateDifficultyHistoryCSV.php
<?php
// This script uses the blockstream explorer API
// It iterates through every block that was minted after a difficulty retarget height
// and if the difficulty changed compared to the previous epoch, will print it in CSV format
$ch = curl_init();
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
$currentBlockHeight = 38304; // one epoch before the first difficulty change (speed up the script by reducing API calls)
$currentBlockHash = NULL;
$currentDifficulty = 1;
// determine where to stop looking for new difficulty epochs
curl_setopt($ch, CURLOPT_URL, "https://blockstream.info/api/blocks/tip/height");
$chainTipHeight = curl_exec($ch);
$maxBlockHeight = $chainTipHeight - ($chainTipHeight % 2016) - 2016;
echo "\nBlock Height,Date,Difficulty\n";
echo "0,2009-01-03,1\n";
while ($currentBlockHeight < $maxBlockHeight) { // there are more difficulty retargets
$currentBlockHeight += 2016;
curl_setopt($ch, CURLOPT_URL, "https://blockstream.info/api/block-height/$currentBlockHeight");
$currentBlockHash = curl_exec($ch);
curl_setopt($ch, CURLOPT_URL, "https://blockstream.info/api/block/$currentBlockHash");
$block = json_decode(curl_exec($ch));
if ($block->difficulty != $currentDifficulty) {
echo $block->height . "," . date("Y-m-d", $block->timestamp) . "," . $block->difficulty . "\n";
}
}