Public
Edited
Dec 14, 2023
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
daily_orders_product
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
daily_orders
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
stores
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function joinDatasets(dailyOrders, stores)
{
return dailyOrders.map(order => {
const store = stores.find(s => s.id === order.store_id);
return store ? { ...order, ...store } : order;
});
}
Insert cell
Insert cell
dailyOrders_stores = joinDatasets(daily_orders, stores);
Insert cell
Insert cell
function getDifferenceFromMean()
{
const totalRevenue = dailyOrders_stores.reduce((sum, order) => sum + order.revenue, 0);
const averageRevenue = totalRevenue / stores.length;
const revenueByCity = {};
dailyOrders_stores.forEach(item => {
if (!revenueByCity[item.city]) revenueByCity[item.city] = 0;
revenueByCity[item.city] += item.revenue;
});

const cityRevenueComparison = [];
for (const city in revenueByCity)
{
cityRevenueComparison.push({
city,
differenceFromAverage: revenueByCity[city] - averageRevenue
});
}
return cityRevenueComparison;
}
Insert cell
Insert cell
cityRevenueComparison = getDifferenceFromMean();
Insert cell
Insert cell
Insert cell
Insert cell
daily_orders_product_copy = daily_orders_product.map(d => ({
...d,
order_date: new Date(d.order_date) // Copy the data too
}));
Insert cell
Insert cell
function calculateMeanRevenues()
{
// Group data by month and category
const groupedData = {};

daily_orders_product_copy.forEach(order => {

// Here I get the Year and the Month of the order
const year = order.order_date.getFullYear();
const month = order.order_date.getMonth() + 1; // We add 1 because months go from 0 to 11
// Here I create an identifier for the agrupation, like Category_Year-Month
const group_id = `${order.category}_${year}-${month}`;
console.log(group_id);
// Here I create the aggrupations of category-year-month
if (!groupedData[group_id]) {
groupedData[group_id] = {
totalRevenue: 0,
count: 0,
date: new Date(year, month - 1, 1).toISOString().split('T')[0],
category: order.category
};
}
// Add the revenues to the total
groupedData[group_id].totalRevenue += order.revenue;
groupedData[group_id].count++;
});

// Get de means and create dataset
const averageRevenueByCategoryAndMonthYear = Object.values(groupedData).map(group => {
return {
Category: group.category,
Date: group.date,
MeanRevenue: group.totalRevenue / group.count
};
});

return averageRevenueByCategoryAndMonthYear;
}

Insert cell
Insert cell
monthlyMedianRevenuesPerCategory = calculateMeanRevenues();
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function calculateMeanRevenueByYear()
{
// Group by year
const groupedByYear = d3.group(daily_orders_product, d => d.order_date.getUTCFullYear());

// Get de mean of revenue for each group
const medianRevenueByYear = {};
groupedByYear.forEach((values, year) => {
let revenues = values.map(d => d.revenue);
medianRevenueByYear[year] = d3.median(revenues);
});

return medianRevenueByYear;
}
Insert cell
Insert cell
medianRevenuePerYear = calculateMeanRevenueByYear();
Insert cell
Insert cell
function colorBasedOnDeviation(deviation)
{
if (deviation > 0)
{
// REVENUE HIGHER THAN MEAN
if (deviation > 10000) return "darkgreen"; // GREAT POSITIVE DESVIATION
else if (deviation > 5000) return "green"; // MODERATE POSITIVE DESVIATION
else return "lightgreen"; // LOW POSITIVE DESVIATION
}
else
{
// REVENUE LOWER THAN MEAN
if (deviation < -10000) return "darkred"; // GREAT NEGATIVE DESVIATION
else if (deviation < -5000) return "red"; // MODERATE NEGATIVE DESVIATION
else return "pink"; // LOW NEGATIVE DESVIATION
}
}
Insert cell
Insert cell
legendData = [
{ color: "darkgreen", label: "Above 10000 more" },
{ color: "green", label: "Between 5000 and 10000 more" },
{ color: "lightgreen", label: "Below 5000 more" },
{ color: "pink", label: "Below 5000 less" },
{ color: "red", label: "Between 5000 and 10000 less" },
{ color: "darkred", label: "Above 10000 less" }
];

Insert cell
Insert cell
function addLegendToPlot(plot)
{
const svg = d3.select(plot);
const legend = svg.append("g")
.attr("class", "legend")
.attr("transform", `translate(10, ${0})`);

let offsetX = 75;
const spacing = 150; // Spacing between legend items

legendData.forEach((item, index) => {
const group = legend.append("g")
.attr("transform", `translate(${offsetX}, 0)`);

group.append("rect")
.attr("x", 0)
.attr("width", 18)
.attr("height", 18)
.style("fill", item.color);

group.append("text")
.attr("x", 90)
.attr("y", 9)
.attr("dy", ".35em")
.text(item.label);

offsetX += 20 + spacing;
});
}
Insert cell
Insert cell
class MonthLine extends Plot.Mark
{
static defaults = {stroke: "currentColor", strokeWidth: 1};
constructor(data, options = {}) {
const {x, y} = options;
super(data, {x: {value: x, scale: "x"}, y: {value: y, scale: "y"}}, options, MonthLine.defaults);
}
render(index, {x, y}, {x: X, y: Y}, dimensions) {
const {marginTop, marginBottom, height} = dimensions;
const dx = x.bandwidth(), dy = y.bandwidth();
return htl.svg`<path fill=none stroke=${this.stroke} stroke-width=${this.strokeWidth} d=${
Array.from(index, (i) => `${Y[i] > marginTop + dy * 1.5 // is the first day a Monday?
? `M${X[i] + dx},${marginTop}V${Y[i]}h${-dx}`
: `M${X[i]},${marginTop}`}V${height - marginBottom}`)
.join("")
}>`;
}
}
Insert cell
function calendar({
date = Plot.identity,
inset = 0.5,
...options
} = {}) {
let D;
return {
fy: {transform: (data) => (D = Plot.valueof(data, date, Array)).map((d) => d.getUTCFullYear())},
x: {transform: () => D.map((d) => d3.utcWeek.count(d3.utcYear(d), d))},
y: {transform: () => D.map((d) => d.getUTCDay())},
inset,
...options
};
}
Insert cell
Insert cell
Insert cell
Plot.plot({
// General config
title: "Total revenue by city compared to the median",
width: 1200,
margin: 100,
height: 750,
label: null,
x: {
axis: "top",
margin: 50,
label: "← Below Average · Difference from Average Revenue · Above Average →",
labelAnchor: "center",
tickFormat: d3.format(".0f"),
},
color: {
scheme: "RdBu", // Red and Blue
type: "categorical"
},
marks: [
// Create the bars for each city
Plot.barX(cityRevenueComparison, {
x: "differenceFromAverage",
y: "city",
fill: (d) => d.differenceFromAverage > 0,
sort: { y: "x" }
}),
Plot.gridX({ stroke: "white", strokeOpacity: 0.5 }),
d3.groups(cityRevenueComparison, (d) => d.differenceFromAverage > 0)
.map(([aboveAverage, cities]) => [
Plot.axisY({
x: 0,
ticks: cities.map((d) => d.city),
tickSize: 0,
anchor: aboveAverage ? "left" : "right"
}),
Plot.textX(cities, {
x: "differenceFromAverage",
y: "city",
text: ((f) => (d) => f(d.differenceFromAverage))(d3.format(".2f")),
textAnchor: aboveAverage ? "start" : "end",
dx: aboveAverage ? 4 : -4,
})
]),
Plot.ruleX([0]) // Line that indicates the mean
]
})
Insert cell
Insert cell
Plot.plot({
title: "Average revenue per month and category",
style: "overflow: visible",
x: {
type: "time",
format: "%b %Y", // 'Ene 2023'
},
y: {grid: true},
marks: [
Plot.ruleY([0]),
Plot.lineY(monthlyMedianRevenuesPerCategory , {x: "Date", y: "MeanRevenue", stroke: "Category", tip:"x"}),
Plot.text(monthlyMedianRevenuesPerCategory , Plot.selectLast({x: "Date", y: "MeanRevenue", z: "Category", text: "Category", textAnchor: "start", dx: 3}))
]
})
Insert cell
Insert cell
Insert cell
CreatePlot();
Insert cell
function CreatePlot()
{
// Start and end of the plot
const start = d3.utcDay.offset(d3.min(daily_orders_product, (d) => d.order_date)); // exclusive
const end = d3.utcDay.offset(d3.max(daily_orders_product, (d) => d.order_date)); // exclusive

// CREATING THE PLOT
const plot = Plot.plot({
width: 1152,
height: d3.utcYear.count(start, end) * 160,
axis: null,
padding: 0,
marginTop: 50,
//marginBottom:75,
x: {
domain: d3.range(53)
},
y: {
axis: "left",
domain: [-1, 0, 1, 2, 3, 4, 5, 6], // DAYS OF WEEK TO SHOW
ticks: [0, 1, 2, 3, 4, 5, 6],
tickSize: 0,
tickFormat: Plot.formatWeekday()
},
fy: {
padding: 0.1,
reverse: true
},
marks: [
// DRAWING THE YEAR LABELS
Plot.text(
d3.utcYears(d3.utcYear(start), end),
calendar({text: d3.utcFormat("%Y"), frameAnchor: "right", x: 0, y: -1, dx: -20})
),

// DRAWING THE MONTH LABELS
Plot.text(
d3.utcMonths(d3.utcMonth(start), end).map(d3.utcMonday.ceil),
calendar({text: d3.utcFormat("%b"), frameAnchor: "left", y: -1})
),

// DRAWING A CELL FOR EACH DAY WITH THE CORRESPONDING COLOR
Plot.cell(
daily_orders_product,
calendar({
date: "order_date",
fill: (d) => {
const year = d.order_date.getUTCFullYear();
const deviation = d.revenue - medianRevenuePerYear[year];

return colorBasedOnDeviation(deviation);
}
})
),

// DRAW MONTH SEPARATORS
new MonthLine(
d3.utcMonths(d3.utcMonth(start), end)
.map((d) => d3.utcDay.offset(d, d.getUTCDay() === 0 ? 1
: d.getUTCDay() === 6 ? 2
: 0)),
calendar({stroke: "white", strokeWidth: 3})
),

// DRAW THE DAY NUMBERS ON THE CELLS
Plot.text(
d3.utcDays(start, end),
calendar({text: d3.utcFormat("%-d")})
)
]
});

// ADDING THE CUSTOM LEGEND TO THE PLOT
addLegendToPlot(plot);
return plot;
}

Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more