【カスタム可視化サンプル】カレンダー上にメジャーと任意の祝日を表示
このページの一番下にGeminiで生成したサンプルコードを置いてます。
完成イメージ カレンダーグラフ
https://scrapbox.io/files/691186a13d7c0fd2f10b4ef7.png
概要
カレンダー上に数値をプロット
任意で設定できる祝日やイベントを加味した傾向を視覚的に把握できる。
必要なデータ
1番目のディメンションには必ずdateが必要
2番目のディメンションは任意で祝日やイベント名のディメンションが必要
メジャーは3つまで設定可能
https://scrapbox.io/files/691187131ba06ec3dcda0741.png
主な機能
カレンダーのセル上にカラースケール表示。対象のメジャーを選択できる。まずここで一番注目させたいポイントを可視化。
https://scrapbox.io/files/691188e017218c0924445bd4.png
https://scrapbox.io/files/6911892c741978850c2552eb.png
メジャーをテキスト・棒グラフで表示し確認できる。
https://scrapbox.io/files/6911899e5f7bae86fe30739d.png https://scrapbox.io/files/691189fa993c4227e940c815.png https://scrapbox.io/files/69118a256020456ddb0ebf44.png
活用例(上記の画像はあくまでサンプルの数値なので、以下の洞察当てはまらないです。)
実施したマーケティングキャンペーンの成果が出たか確認する
今後の適切な営業人数を策定するために、3連休のときは2連休と比較して1日あたりの来客数が少なくなるか確認
祝日やイベントの特性に関連した予算案などの策定
同じ3連休でも「3日: 文化の日」と「24日: 勤労感謝の日」でもブラックフライデーに近い「勤労感謝の日」の方が売上が悪いので、今後はブラックフライデーに近いときは予めコストを抑えよう。など
祝日・イベント用の2つめのディメンションの設定例
祝日やイベント以外でも使えるかも
営業日
天気
大きな障害、メンテナンス情報
サンプルコード
code:manifest.lkml
visualization: {
id: "custom_calendar_viz"
label: "by Gemini カレンダー"
file: "calendar_viz.js"
# dependencies: [] # 外部ライブラリは使用しないため省略
# sri_hash: "..." # ホスティングしないため不要
}
code:calendar_viz.js
// カレンダー表示用カスタム可視化スクリプト
// このスクリプトはLookerのデータを受け取り、カレンダー形式で描画します。
looker.plugins.visualizations.add({
// -----------------------------------------------------------
// 1. 設定オプションの定義
// Lookerの編集画面右側に表示される設定メニューを定義します。
// -----------------------------------------------------------
options: {
// --- カレンダー全体の書式設定 ---
weekendColoring: {
type: 'boolean',
label: '週末に色を付ける',
default: true,
section: 'カレンダー',
order: 1
},
weekdayLanguage: {
type: 'string',
label: '曜日の表示言語',
display: 'select',
values: [
{'日本語': 'ja'},
{'English': 'en'}
],
default: 'ja',
section: 'カレンダー',
order: 2
},
// --- イベント名の書式設定 ---
eventFontColor: {
type: 'string',
label: 'イベントの色',
display: 'color',
default: '#333333',
section: 'カレンダー',
order: 3
},
eventFontSize: {
type: 'number',
label: 'イベントのフォントサイズ (px)',
default: 11,
section: 'カレンダー',
order: 4
},
eventFontBold: {
type: 'boolean',
label: 'イベントを太字にする',
default: false,
section: 'カレンダー',
order: 5
},
// --- メジャー(数値)の表示設定 ---
// ※ 3つのメジャー設定を「メジャー」セクションにまとめました。
// orderを調整して、設定画面上で順番に並ぶようにしています。
// メジャー1の設定
legendSection1: {type: 'string', label: '▼▼▼メジャー1▼▼▼', display: 'heading', order: 9, section: 'メジャー'}, //見出し用
showMeasureValue1: { type: 'boolean', label: '表示', default: true, section: 'メジャー', order: 10, display_size: 'third' },
measureFontSize1: { type: 'number', label: 'フォントサイズ (px)', default: 12, section: 'メジャー', order: 11 , display_size: 'third' },
measureFontColor1: { type: 'string', label: 'フォント色', display: 'color', default: '#333333', section: 'メジャー', order: 12 , display_size: 'third' },
// メジャー2の設定
legendSection2: {type: 'string', label: '▼▼▼メジャー2▼▼▼', display: 'heading', order: 19, section: 'メジャー'}, //見出し用
showMeasureValue2: { type: 'boolean', label: '表示', default: true, section: 'メジャー', order: 20 , display_size: 'third' },
measureFontSize2: { type: 'number', label: 'フォントサイズ (px)', default: 12, section: 'メジャー', order: 21 , display_size: 'third' },
measureFontColor2: { type: 'string', label: 'フォント色', display: 'color', default: '#333333', section: 'メジャー', order: 22 , display_size: 'third' },
// メジャー3の設定
legendSection3: {type: 'string', label: '▼▼▼メジャー3▼▼▼', display: 'heading', order: 29, section: 'メジャー'}, //見出し用
showMeasureValue3: { type: 'boolean', label: '表示', default: true, section: 'メジャー', order: 30, display_size: 'third' },
measureFontSize3: { type: 'number', label: 'フォントサイズ (px)', default: 12, section: 'メジャー', order: 31 , display_size: 'third' },
measureFontColor3: { type: 'string', label: 'フォント色', display: 'color', default: '#333333', section: 'メジャー', order: 32, display_size: 'third' },
// --- 棒グラフの表示設定 ---
showBarChart: {
type: 'boolean',
label: '縦棒グラフで表示',
default: false,
section: '棒グラフ',
order: 1
},
barWidth: { type: 'number', label: '棒グラフの太さ (px)', default: 5, section: '棒グラフ', order: 2 },
barColor1: { type: 'string', label: 'メジャー1の色', display: 'color', default: '#3498db', section: '棒グラフ', order: 3 },
barColor2: { type: 'string', label: 'メジャー2の色', display: 'color', default: '#e74c3c', section: '棒グラフ', order: 4 },
barColor3: { type: 'string', label: 'メジャー3の色', display: 'color', default: '#2ecc71', section: '棒グラフ', order: 5 },
// --- 背景色スケール(ヒートマップ)設定 ---
showBackgroundColorScale: {
type: 'boolean',
label: '背景色の濃淡で表示',
default: false,
section: '背景色スケール',
order: 1
},
backgroundColorMeasure: {
type: 'string',
label: '└ 対象メジャー',
display: 'select',
values: [], // データ取得後に動的に選択肢を設定します
default: '',
section: '背景色スケール',
order: 2
},
minColor: { type: 'string', label: '└ 最小値の色', display: 'color', default: '#e6f7ff', section: '背景色スケール', order: 3 },
midColor: { type: 'string', label: '└ 中間値の色', display: 'color', default: '#74c0fc', section: '背景色スケール', order: 4 },
maxColor: { type: 'string', label: '└ 最大値の色', display: 'color', default: '#1864ab', section: '背景色スケール', order: 5 }
},
// -----------------------------------------------------------
// 2. 初期化処理 (create)
// Vizが表示される最初に1回だけ実行される処理です。
// コンテナを作成したり、初期状態を設定したりします。
// -----------------------------------------------------------
create: function(element, config) {
// カレンダーを表示するためのメインコンテナを作成
this.container = element.appendChild(document.createElement("div"));
this.container.className = "calendar-viz-container";
// 初期表示する年月(デフォルトは現在の年月など適宜調整可能)
this.currentDate = new Date();
},
// -----------------------------------------------------------
// 3. 描画処理 (updateAsync)
// データが変更されたり、設定が変更されるたびに実行されます。
// ここで実際のカレンダー描画を行います。
// -----------------------------------------------------------
updateAsync: function(data, element, config, queryResponse, details, done) {
this.clearErrors();
// 必須データのチェック: 最低1つの日付ディメンションが必要
if (queryResponse.fields.dimensions.length < 1) {
this.addError({ title: "ディメンション不足", message: "日付として解釈できるディメンションを1つ以上選択してください。" });
return;
}
// コンテナをクリアして再描画の準備
const container = this.container;
container.innerHTML = '';
// --- データ定義の取得 ---
// 1つ目のディメンションを「日付」、2つ目があれば「イベント名」として扱います
const dateDimension = queryResponse.fields.dimensions0.name; const eventDimension = queryResponse.fields.dimensions.length > 1 ? queryResponse.fields.dimensions1.name : null; // メジャーは最大3つまで使用します
const measures = queryResponse.fields.measures.slice(0, 3);
// --- 設定オプションの動的更新 ---
// 「背景色スケール」の対象メジャー選択肢を、実際のデータに基づいて更新します
const measureOptions = measures.map(measure => {
const option = {};
return option;
});
let newOptions = this.options;
newOptions.backgroundColorMeasure.values = measureOptions;
if (measures.length > 0 && !config.backgroundColorMeasure) {
newOptions.backgroundColorMeasure.default = measures0.name; }
this.trigger('registerOptions', newOptions);
// --- データの事前処理 ---
// グラフのスケール計算用に、各メジャーの最小値・最大値を計算します
let measureStats = {
all: { min: Infinity, max: -Infinity }
};
measures.forEach(m => {
measureStatsm.name = { min: Infinity, max: -Infinity }; });
// Lookerから受け取った配列データを、日付文字列をキーとした連想配列(Map)に変換します
// これによりカレンダーの日付セルからデータを高速に参照できます
const dataByDate = {};
data.forEach(row => {
// 日付型データのタイムゾーン部分を切り捨てて「YYYY-MM-DD」形式のキーを作成
const dateStr = dateCell.value.split('T')0; const measureValues = measures.map(m => rowm.name.value); event: eventDimension ? LookerCharts.Utils.textForCell(roweventDimension) : '', measures: measureValues,
};
// 統計情報の更新(最小値・最大値)
measureValues.forEach((value, i) => {
if (value !== null) {
measureStats.all.min = Math.min(measureStats.all.min, value);
measureStats.all.max = Math.max(measureStats.all.max, value);
const measureName = measuresi.name; }
});
});
// --- カレンダーヘッダーの描画 ---
// 年月表示と、前月/翌月への切り替えボタンを配置します
const header = container.appendChild(document.createElement('div'));
header.className = 'calendar-header';
const prevButton = header.appendChild(document.createElement('button'));
prevButton.innerHTML = '‹';
prevButton.onclick = () => {
this.currentDate.setMonth(this.currentDate.getMonth() - 1);
this.updateAsync(data, element, config, queryResponse, details, done);
};
const monthDisplay = header.appendChild(document.createElement('h2'));
monthDisplay.innerHTML = ${this.currentDate.getFullYear()}年 ${this.currentDate.getMonth() + 1}月;
const nextButton = header.appendChild(document.createElement('button'));
nextButton.innerHTML = '›';
nextButton.onclick = () => {
this.currentDate.setMonth(this.currentDate.getMonth() + 1);
this.updateAsync(data, element, config, queryResponse, details, done);
};
// --- カレンダーグリッドの描画 ---
const calendar = container.appendChild(document.createElement('div'));
calendar.className = 'calendar-grid';
// 曜日ヘッダーの描画
const weekdays = config.weekdayLanguage === 'ja'
weekdays.forEach((day, i) => {
const weekdayCell = calendar.appendChild(document.createElement('div'));
weekdayCell.className = 'weekday-header';
weekdayCell.textContent = day;
// 週末の色付け設定がONの場合、土日に色を付けます
if (config.weekendColoring) {
if (i === 0) weekdayCell.style.color = 'red'; // 日曜
if (i === 6) weekdayCell.style.color = 'blue'; // 土曜
}
});
// カレンダーの日付セル計算
const year = this.currentDate.getFullYear();
const month = this.currentDate.getMonth();
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
// 月初めの空白セルを埋める
for (let i = 0; i < firstDay.getDay(); i++) {
calendar.appendChild(document.createElement('div'));
}
// 1日から月末までのセルを描画するループ
for (let day = 1; day <= lastDay.getDate(); day++) {
const dateCell = calendar.appendChild(document.createElement('div'));
dateCell.className = 'date-cell';
const currentDate = new Date(year, month, day);
const dayOfWeek = currentDate.getDay();
// データ参照用のキーを作成 (YYYY-MM-DD)
const dateStr = ${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')};
// 日付数字の表示
const dayNumber = dateCell.appendChild(document.createElement('div'));
dayNumber.className = 'day-number';
dayNumber.textContent = day;
if (config.weekendColoring) {
if (dayOfWeek === 0) dayNumber.style.color = 'red';
if (dayOfWeek === 6) dayNumber.style.color = 'blue';
}
// その日付に対応するデータがある場合の描画処理
const cellData = dataByDatedateStr; // 1. イベント名の描画
if (cellData.event) {
const eventDiv = dateCell.appendChild(document.createElement('div'));
eventDiv.className = 'event-name';
eventDiv.textContent = cellData.event;
// 設定に基づきスタイルを適用
eventDiv.style.color = config.eventFontColor;
eventDiv.style.fontSize = ${config.eventFontSize}px;
eventDiv.style.fontWeight = config.eventFontBold ? 'bold' : 'normal';
}
// メジャーデータがある場合
if (cellData.measures.length > 0) {
// 2. 数値テキストの描画
const measureValuesContainer = dateCell.appendChild(document.createElement('div'));
measureValuesContainer.className = 'measure-values-container';
cellData.measures.forEach((value, i) => {
// 各メジャーの表示設定がONの場合のみ描画
if (value !== null && config[showMeasureValue${i+1}]) {
const measureSpan = measureValuesContainer.appendChild(document.createElement('span'));
measureSpan.className = 'measure-value';
// ラベルと値を表示 (例: "売上: 1,000")
measureSpan.textContent = ${measures[i].label_short || measures[i].label}: ${value.toLocaleString()};
measureSpan.style.fontSize = ${config[measureFontSize${i+1}]}px;
measureSpan.style.color = config[measureFontColor${i+1}]
}
});
// 3. 棒グラフの描画 (設定がONの場合)
if (config.showBarChart) {
const barContainer = dateCell.appendChild(document.createElement('div'));
barContainer.className = 'bar-container';
cellData.measures.forEach((value, i) => {
if (value !== null) {
const bar = barContainer.appendChild(document.createElement('div'));
bar.className = 'bar-chart';
// 全体の最大・最小値から相対的な高さを計算
const totalRange = measureStats.all.max - measureStats.all.min;
const barHeight = totalRange > 0 ? ((value - measureStats.all.min) / totalRange) * 100 : 0;
bar.style.height = ${barHeight}%;
bar.style.backgroundColor = config[barColor${i+1}];
bar.style.width = ${config.barWidth}px;
}
});
}
// 4. 背景色スケールの適用 (設定がONの場合)
if (config.showBackgroundColorScale && config.backgroundColorMeasure) {
const selectedMeasureName = config.backgroundColorMeasure;
const selectedMeasureIndex = measures.findIndex(m => m.name === selectedMeasureName);
if (selectedMeasureIndex !== -1) {
if (selectedMeasureValue !== null) {
// 値に基づいて色を補間する関数
const getColor = (value) => {
if (!stats) return config.midColor;
const range = stats.max - stats.min;
if (range === 0) return config.midColor;
const percent = (value - stats.min) / range;
// 中間値を境に2段階で色を補間
if (percent < 0.5) {
return interpolateColor(config.minColor, config.midColor, percent * 2);
} else {
return interpolateColor(config.midColor, config.maxColor, (percent - 0.5) * 2);
}
};
dateCell.style.backgroundColor = getColor(selectedMeasureValue);
}
}
}
}
}
}
// --- CSSスタイルの適用 ---
// カレンダーの見た目を整えるためのCSSを定義します
const style = container.appendChild(document.createElement('style'));
style.innerHTML = `
.calendar-viz-container { font-family: sans-serif; height: 100%; display: flex; flex-direction: column; }
.calendar-header { display: flex; justify-content: space-between; align-items: center; padding: 5px 10px; }
.calendar-header h2 { margin: 0; font-size: 16px; }
.calendar-header button { border: 1px solid #ccc; background-color: #f7f7f7; border-radius: 4px; cursor: pointer; padding: 2px 8px; } /* 7列のカレンダーグリッド */
.calendar-grid { flex-grow: 1; display: grid; grid-template-columns: repeat(7, 1fr); grid-template-rows: auto repeat(5, 1fr); gap: 1px; background-color: #ddd; } .weekday-header { text-align: center; padding: 4px; background-color: #f0f0f0; font-weight: bold; font-size: 12px; } .date-cell { background-color: white; padding: 4px; position: relative; display: flex; flex-direction: column; overflow: hidden; }
.day-number { font-size: 12px; font-weight: bold; margin-bottom: 2px; }
.event-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.measure-values-container { display: flex; flex-direction: column; gap: 2px; margin-top: 2px; }
.measure-value { font-size: 12px; white-space: nowrap; }
/* 棒グラフはセルの右下に配置 */
.bar-container { position: absolute; right: 2px; bottom: 2px; height: 50%; display: flex; align-items: flex-end; gap: 1px; opacity: 0.7; }
.bar-chart { min-height: 1px; }
`;
// ヘルパー関数: 2つの色を指定された割合でブレンドします
function interpolateColor(color1, color2, factor) {
if (factor === undefined) factor = 0.5;
const hexToRgb = (hex) => {
return result ? [parseInt(result1, 16), parseInt(result2, 16), parseInt(result3, 16)] : null; };
const c1 = hexToRgb(color1);
const c2 = hexToRgb(color2);
if (!c1 || !c2) return '#FFFFFF';
const r = Math.round(c10 + factor * (c20 - c10)); const g = Math.round(c11 + factor * (c21 - c11)); const b = Math.round(c12 + factor * (c22 - c12)); return rgb(${r}, ${g}, ${b});
}
// 描画完了をLookerに通知
done();
}
});
作成者 Kentaro Tanaka.icon