Last active
March 4, 2024 07:39
-
-
Save artlung/748b03caef659f682fe399004cbca6ab to your computer and use it in GitHub Desktop.
ArtLung Blog "Roanoke" WordPress Theme Blog Visualization Shortcode. Not packed as a plugin, would need to be modified to add to your blog. I may at some point package it up into a plugin form but feel free to extend, modify, share.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Goes with RoanokeVisualization - compiles to CSS | |
// $sans-serif-front is imported from _variables.scss | |
$postsFill: lightgreen; | |
$instagramFill: rgba(239, 214, 125); | |
$deliciousFill: skyblue; | |
$bg: transparent; | |
ol.visualization-key { | |
list-style: none; | |
padding: 0; | |
margin: 0; | |
display: flex; | |
flex-wrap: wrap; | |
gap: 0.5rem; | |
font-family: $sans-serif-font; | |
margin-block-start: 1rem; | |
margin-block-end: 1rem; | |
li { | |
font-size: 0.8rem; | |
min-width: 15ch; | |
margin: 0; | |
} | |
span { | |
display: block; | |
padding: 0.5ch 1ch; | |
&.instagram { | |
background-color: $instagramFill; | |
} | |
&.posts { | |
background-color: $postsFill; | |
} | |
&.delicious { | |
background-color: $deliciousFill; | |
} | |
} | |
} | |
// This applies to roanoke archive year a | |
*[data-has-css-vars] { | |
box-sizing: border-box; | |
font-family: $sans-serif-font; | |
color: #000; | |
min-width: 12ch; | |
min-height: 1rem; | |
padding: 0.2rem; | |
font-size: 0.8rem; | |
border-width: 0 0 1px 0; | |
border-style: dotted; | |
border-color: #ccc; | |
text-decoration: none; | |
line-height: 1; | |
&:hover { | |
text-decoration: none; | |
} | |
} | |
// we need --postPct | |
// we need --instagramPct | |
// we need --deliciousPct | |
*[data-has-css-vars] { | |
background: | |
linear-gradient(90deg, | |
$instagramFill var(--instagramPct), | |
$deliciousFill var(--instagramPct) calc(var(--instagramPct) + var(--deliciousPct)), | |
transparent 0 | |
), | |
linear-gradient(90deg, | |
$postsFill var(--postPct), | |
transparent 0 | |
); | |
} | |
*[data-has-css-vars] i { | |
font-style: normal; | |
} | |
/* experimental do a circle based pie chart */ | |
.circle-view *[data-has-css-vars] { | |
width: 5rem; | |
height: 5rem; | |
min-width: unset; | |
border-width: 1px; | |
border-style: solid; | |
border-radius: 50%; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
white-space: nowrap; | |
background: conic-gradient($postsFill var(--postPct), $bg var(--postPct)); | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
require_once('WordPressArchiveLink.php'); | |
require_once('RoanokeVisualization.php'); | |
/** | |
** Shortcodes | |
**/ | |
// [blog_visualization] | |
add_shortcode('blog_visualization', function() { | |
return RoanokeVisualization::blogVisualization(); | |
}); | |
// [blog_visualization_key] | |
add_shortcode('blog_visualization_key', function($atts) { | |
return RoanokeVisualization::blogVisualizationKey(); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Class RoanokeVisualization | |
* Creates a visualization of blog posts by year and month | |
* and breaks out posts, and posts with Instagram and Delicious tags | |
* | |
* Uses wp_get_archives to get the data | |
* Uses wp_cache to store the data from expensive operations | |
* | |
* Must be set as a shortcode in WordPress functions.php | |
* Include this file in functions.php | |
* And include WordPressArchiveLink.php too | |
*/ | |
class RoanokeVisualization { | |
const CACHE_GROUP = 'roanoke'; | |
const EXPENSIVE_CACHE_DURATION = 60 * 60 * 24 * 7; // for a week | |
// don't keep current year data too long: 6 hours | |
const CACHE_DURATION_FOR_CURRENT_MONTH_YEAR = 60 * 60 * 6; | |
// keep current year data for a day | |
const CACHE_DURATION_FOR_CURRENT_YEAR = 60 * 60 * 24; | |
// during development, keep the overall html for 5 minutes | |
const OVERALL_HTML_CACHE_DURATION = 60 * 5; | |
/** | |
* creates a cache key from method signature and arguments | |
* @param ...$args | |
* | |
* @return string | |
*/ | |
protected static function createCacheKey( ...$args ): string { | |
return implode('-', $args); | |
} | |
/** | |
* @param $year | |
* @param $month | |
* | |
* @return int | |
*/ | |
protected static function getCacheDuration($year = null, $month = null) { | |
$currentMonth = date('m'); | |
$currentYear = date('Y'); | |
if ($year == $currentYear) { | |
if ($month == $currentMonth) { | |
return self::CACHE_DURATION_FOR_CURRENT_MONTH_YEAR; | |
} | |
return self::CACHE_DURATION_FOR_CURRENT_YEAR; | |
} | |
return self::EXPENSIVE_CACHE_DURATION; | |
} | |
/** | |
* @return string | |
*/ | |
public static function blogVisualization(): string { | |
$cacheKey = self::createCacheKey( __CLASS__, __FUNCTION__ ); | |
$cache = wp_cache_get( $cacheKey, self::CACHE_GROUP ); | |
if ( $cache ) { | |
return $cache; | |
} | |
$out = ''; | |
/* show archive information */ | |
$args = [ | |
'type' => 'monthly', | |
'format' => 'custom', | |
'before' => '', | |
'after' => '', | |
'show_post_count' => true, | |
'echo' => 0, | |
'order' => 'ASC' | |
]; | |
// wp_get_archives link | |
// https://developer.wordpress.org/reference/functions/wp_get_archives/ | |
$resulthtml = wp_get_archives( $args ); | |
$archives = array_map( 'trim', explode( "\n", $resulthtml ) ); | |
$archiveYears = []; | |
$archiveLinks = []; | |
$yearTotals = []; | |
$lowestCount = /* max integer */ | |
PHP_INT_MAX; | |
$highestCount = 0; | |
foreach ( $archives as $item ) { | |
if ( trim( $item ) !== '' ) { | |
$archiveLink = new WordPressArchiveLink( trim( $item ) ); | |
if ( $archiveLink->getCount() < $lowestCount ) { | |
$lowestCount = $archiveLink->getCount(); | |
} | |
if ( $archiveLink->getCount() > $highestCount ) { | |
$highestCount = $archiveLink->getCount(); | |
} | |
$archiveLinks[] = $archiveLink; | |
$yearTotals[ $archiveLink->getYear() ] += $archiveLink->getCount(); | |
$archiveYears[ $archiveLink->getYear() ][ $archiveLink->getMonth() ] = $archiveLink; | |
} | |
} | |
$minPostsPerYear = PHP_INT_MAX; | |
$maxPostsPerYear = 0; | |
foreach ( $yearTotals as $count ) { | |
if ( $count < $minPostsPerYear ) { | |
$minPostsPerYear = $count; | |
} | |
if ( $count > $maxPostsPerYear ) { | |
$maxPostsPerYear = $count; | |
} | |
} | |
$earliestLinkHtml = $archiveLinks[0]; | |
$latestLinkHtml = $archiveLinks[ count( $archiveLinks ) - 1 ]; | |
$yearsInclusive = range( $earliestLinkHtml->getYear(), $latestLinkHtml->getYear() ); | |
$monthsInclusive = range( 1, 12 ); | |
// pad monthsInclusive with 0s | |
$monthsInclusive = array_map( function ( $month ) { | |
return str_pad( $month, 2, '0', STR_PAD_LEFT ); | |
}, $monthsInclusive ); | |
$yearRange = 0; | |
$percentage = 0; | |
foreach ( $yearsInclusive as $referenceYear ) { | |
$out .= '<div class="roanoke-archive-year">'; | |
$yearLink = sprintf( "/blog/%s/", $referenceYear ); | |
// %d posts in the year %s. | |
if ( array_key_exists( $referenceYear, $yearTotals ) ) { | |
$yearTitle = sprintf( "%d posts in the year %s.", $yearTotals[ $referenceYear ], $referenceYear ); | |
$yearRange = $maxPostsPerYear - $minPostsPerYear; | |
$percentage = 100 * ( $yearTotals[ $referenceYear ] - $minPostsPerYear ) / $yearRange; | |
} else { | |
$yearTitle = sprintf( "No posts in the year %s.", $referenceYear ); | |
} | |
// if it's the current year, add "SO FAR!"; | |
if ( $referenceYear == date( 'Y' ) ) { | |
$yearTitle .= " SO FAR!"; | |
} | |
[ $instagramCount, $deliciousCount ] = self::getIGAndDeliciousCountForYear( $referenceYear ); | |
// for the year | |
if ( $instagramCount && $yearRange ) { | |
$instagramPercentage = 100 * ( $instagramCount - $minPostsPerYear ) / $yearRange; | |
} else { | |
$instagramPercentage = 0; | |
} | |
if ( $deliciousCount && $yearRange ) { | |
$deliciousPercentage = 100 * ( $deliciousCount - $minPostsPerYear ) / $yearRange; | |
} else { | |
$deliciousPercentage = 0; | |
} | |
$combinedPercentageAttr = sprintf( | |
'data-has-css-vars style="--postPct:%s;--instagramPct:%s;--deliciousPct:%s"', | |
round( $percentage ) . '%', | |
round( $instagramPercentage ) . '%', | |
round( $deliciousPercentage ) . '%' | |
); | |
$yearAttr = ''; | |
$out .= "<h4><a href='$yearLink' title='$yearTitle' $yearAttr $combinedPercentageAttr>$referenceYear</a></h4>"; | |
$out .= "<ul>"; | |
foreach ( $monthsInclusive as $referenceMonth ) { | |
[ | |
$instagramCount, | |
$deliciousCount | |
] = self::getIGAndDeliciousCountForYearMonth( $referenceYear, $referenceMonth ); | |
$actualLink = $archiveYears[ $referenceYear ][ $referenceMonth ] ?? false; | |
if ( $actualLink ) { | |
if ( $actualLink->getCount() === 1 ) { | |
$monthTitle = sprintf( "%d post in %s %s.", $actualLink->getCount(), $actualLink->getMonthName(), $actualLink->getYear() ); | |
} else { | |
$monthTitle = sprintf( "%d posts in %s %s.", $actualLink->getCount(), $actualLink->getMonthName(), $actualLink->getYear() ); | |
} | |
$range = $highestCount - $lowestCount; | |
$percentage = 100 * ( $actualLink->getCount() - $lowestCount ) / $range; | |
$style = ''; | |
// for the month | |
if ( $instagramCount ) { | |
$instagramPercentage = 100 * ( $instagramCount - $lowestCount ) / $range; | |
} else { | |
$instagramPercentage = 0; | |
} | |
if ( $deliciousCount ) { | |
$deliciousPercentage = 100 * ( $deliciousCount - $lowestCount ) / $range; | |
} else { | |
$deliciousPercentage = 0; | |
} | |
$monthAttr = sprintf( | |
' data-has-css-vars style="--postPct:%s;--instagramPct:%s;--deliciousPct:%s"', | |
round( $percentage ) . '%', | |
round( $instagramPercentage ) . '%', | |
round( $deliciousPercentage ) . '%' | |
); | |
// if it's the current month, add "SO FAR!"; | |
if ( $actualLink->getYear() == date( 'Y' ) && $actualLink->getMonth() == date( 'm' ) ) { | |
$monthTitle .= " SO FAR!"; | |
} | |
/** @noinspection HtmlUnknownTarget */ | |
$out .= sprintf( "<li><a href=\"%s\" %s title='%s' %s>%s</a></li>", | |
$actualLink->getHref(), | |
$style, | |
$monthTitle, | |
$monthAttr, | |
$actualLink->getMonthName() | |
); | |
} else { | |
$emptyMonthName = date( 'F', mktime( 0, 0, 0, $referenceMonth, 1, $referenceYear ) ); | |
$out .= "<li><a style='color: black;opacity: 0.5'>$emptyMonthName</a></li>"; | |
} | |
} | |
$out .= "</ul>"; | |
$out .= '</div>'; | |
} | |
$out = '<div class="post-visualization">' . $out . '</div>'; | |
$cacheExpiration = self::OVERALL_HTML_CACHE_DURATION; | |
wp_cache_set( $cacheKey, $out, self::CACHE_GROUP, $cacheExpiration ); | |
return $out; | |
} | |
/** | |
* | |
* @return string | |
*/ | |
public static function blogVisualizationKey(): string { | |
$keys = [ | |
'posts' => 'Posts', | |
'instagram' => 'Instagram', | |
'delicious' => 'Delicious', | |
]; | |
foreach ( $keys as $className => $label ) { | |
$keys[ $className ] = sprintf( '<span class="visualization-entry %s">%s</span>', $className, $label ); | |
} | |
$listItems = ''; | |
foreach ( $keys as $className => $label ) { | |
// TODO maybe use $className? | |
$listItems .= sprintf( '<li>%s</li>', $label ); | |
} | |
return '<ol class="visualization-key">' . $listItems . '</ol>'; | |
// echo as ordered list | |
} | |
/** | |
* @param $referenceYear | |
* @param $referenceMonth | |
* | |
* @return array [$instagramCount,$deliciousCount] | |
*/ | |
public static function getIGAndDeliciousCountForYearMonth( $referenceYear, $referenceMonth ): array { | |
$cacheKey = self::createCacheKey( __CLASS__, __FUNCTION__, $referenceYear, $referenceMonth ); | |
$cache = wp_cache_get( $cacheKey, self::CACHE_GROUP ); | |
if ( $cache ) { | |
return $cache; | |
} | |
$deliciousCount = 0; | |
$instagramCount = 0; | |
$args = [ | |
'year' => $referenceYear, | |
'monthnum' => $referenceMonth, | |
'post_type' => 'post', | |
'post_status' => 'publish', | |
'posts_per_page' => - 1, | |
]; | |
$posts = get_posts( $args ); | |
foreach ( $posts as $post ) { | |
if ( has_tag( 'delicious', $post ) && strpos( $post->post_name, 'daily-links' ) > - 1 ) { | |
$deliciousCount ++; | |
} | |
if ( has_tag( 'via-instagram', $post ) ) { | |
$instagramCount ++; | |
} | |
} | |
$cacheExpiration = self::getCacheDuration( $referenceYear, $referenceMonth ); | |
// stash it | |
wp_cache_set( $cacheKey, [ | |
$instagramCount, | |
$deliciousCount | |
], self::CACHE_GROUP, $cacheExpiration ); | |
return [ $instagramCount, $deliciousCount ]; | |
} | |
/** | |
* @param $referenceYear | |
* | |
* @return int[]|mixed | |
*/ | |
public static function getIGAndDeliciousCountForYear( $referenceYear ) { | |
$cacheKey = self::createCacheKey(__CLASS__, __FUNCTION__, $referenceYear); | |
$cache = wp_cache_get($cacheKey, self::CACHE_GROUP); | |
if ($cache) { | |
return $cache; | |
} | |
$deliciousCount = 0; | |
$instagramCount = 0; | |
$args = [ | |
'year' => $referenceYear, | |
'post_type' => 'post', | |
'post_status' => 'publish', | |
'posts_per_page' => -1, | |
]; | |
$posts = get_posts($args); | |
foreach ($posts as $post) { | |
if (has_tag('delicious', $post) && strpos($post->post_name, 'daily-links') > -1) { | |
$deliciousCount++; | |
} | |
if (has_tag('via-instagram', $post)) { | |
$instagramCount++; | |
} | |
} | |
$cacheExpiration = self::getCacheDuration($referenceYear); | |
wp_cache_set($cacheKey, [$instagramCount,$deliciousCount], self::CACHE_GROUP, $cacheExpiration); | |
return [$instagramCount,$deliciousCount]; | |
} | |
/** | |
* Use this to clear cache keys for the group | |
* TODO actually this is not working oops. | |
* @return void | |
*/ | |
public static function clearAllGroupCacheKeys() { | |
$cacheGroup = self::CACHE_GROUP; | |
$cacheKeys = wp_cache_get($cacheGroup, $cacheGroup); | |
if ($cacheKeys) { | |
foreach ($cacheKeys as $cacheKey) { | |
wp_cache_delete($cacheKey, $cacheGroup); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* WordPressArchiveLink parses the output of wp_get_archives | |
* and returns the year, month, count, and href, | |
* very specifically for my own blog and probaby shoud be rethought for someone else's blog. | |
*/ | |
class WordPressArchiveLink { | |
// construct takes a string | |
// the string is like <a href="https://artlung.com/blog/2001/02/">February 2001</a> (9) | |
// we want to get the year of the link | |
// we want the numerical month | |
// we want the count (inside the parentheses) | |
/** | |
* @var mixed | |
*/ | |
private $archiveHtml; | |
function __construct($archiveHtml) { | |
$this->archiveHtml = $archiveHtml; | |
} | |
public function getYear() { | |
$matches = []; | |
$pattern = '/<a href=["\']https:\/\/artlung.com\/blog\/(\d{4})\/\d{2}\/["\']>/'; | |
preg_match($pattern, $this->archiveHtml, $matches); | |
return $matches[1]; | |
} | |
public function getMonth() { | |
$matches = []; | |
$pattern = '/<a href=["\']https:\/\/artlung.com\/blog\/\d{4}\/(\d{2})\/["\']>/'; | |
preg_match($pattern, $this->archiveHtml, $matches); | |
return $matches[1]; | |
} | |
public function getCount() { | |
$matches = []; | |
$pattern = '/\((\d+)\)/'; | |
preg_match($pattern, $this->archiveHtml, $matches); | |
return $matches[1]; | |
} | |
public function getHref() { | |
$matches = []; | |
$pattern = '/<a href=["\'](.*)["\']>/'; | |
preg_match($pattern, $this->archiveHtml, $matches); | |
return $matches[1]; | |
} | |
public function getMonthName() { | |
$month = $this->getMonth(); | |
$monthNames = [ | |
'01' => 'January', | |
'02' => 'February', | |
'03' => 'March', | |
'04' => 'April', | |
'05' => 'May', | |
'06' => 'June', | |
'07' => 'July', | |
'08' => 'August', | |
'09' => 'September', | |
'10' => 'October', | |
'11' => 'November', | |
'12' => 'December', | |
]; | |
return $monthNames[$month]; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment