Skip to content

Instantly share code, notes, and snippets.

@artlung
Last active March 4, 2024 07:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save artlung/748b03caef659f682fe399004cbca6ab to your computer and use it in GitHub Desktop.
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.
// 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));
}
<?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();
});
<?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);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment