Public
Edited
May 6
Insert cell
# Mobile Phones Specifications Explorer

Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
data = FileAttachment("Mobiles_Dataset(2025).csv").csv({ typed: true })


Insert cell
data.slice(0, 5)

Insert cell
Object.keys(data[0])

Insert cell
filtered = data.filter(d =>
d["RAM(GB)"] &&
d["Launched Price (USA)USD"] &&
d["Battery Capacity(mAh)"] &&
d["Company Name"] &&
!isNaN(+d["RAM(GB)"]) &&
!isNaN(+d["Launched Price (USA)USD"]) &&
!isNaN(+d["Battery Capacity(mAh)"])
)

Insert cell
averageRAM = d3.rollups(
filtered,
v => d3.mean(v, d => +d["RAM(GB)"]),
d => d["Launched Year"],
d => d["Company Name"]
).flatMap(([year, entries]) =>
entries.map(([company, avgRam]) => ({
year,
company,
avgRam
}))
)

Insert cell
// Sort the averageRAM array by year (ascending) and avgRam (descending)
sortedData = averageRAM
.slice() // create a copy
.sort((a, b) => {
if (a.year === b.year) return b.avgRam - a.avgRam;
return d3.ascending(a.year, b.year);
});

Insert cell
Insert cell
Plot.plot({
title: "📊 Comparison of RAM by Smartphone Brand Over Time",
width: 800,
height: 500,
grid: true,
x: {
label: "Launched Year",
type: "band"
},
y: {
label: "Average RAM (GB)",
nice: true
},
color: {
legend: true,
label: "Company"
},
marks: [
Plot.barY(averageRAM, {
x: "year",
y: "avgRam",
fill: "company",
title: d => `${d.company} (${d.year})\nAvg RAM: ${d.avgRam.toFixed(2)} GB`
})
]
})

Insert cell
Object.keys(data[0])

Insert cell
function cleanedAndGrouped(data) {
const cleaned = data.filter(d =>
!isNaN(+d["Launched Price (USA)USD"]) &&
!isNaN(+d["Battery (mAh)"]) &&
!isNaN(+d["RAM(GB)"])
);

const grouped = d3.rollups(
cleaned,
v => ({
Avg_Launched_Price_USD: d3.mean(v, d => +d["Launched Price (USA)USD"]),
Avg_Battery_mAh: d3.mean(v, d => +d["Battery Capacity(mAh)"])
}),
d => +d["RAM(GB)"]
).map(([key, value]) => ({
RAM_GB: key,
...value
}));

return grouped;
}

Insert cell
validData = data.filter(d =>
!isNaN(+d["Launched Price (USA)USD"]) &&
!isNaN(+d["Battery Capacity(mAh)"]) &&
!isNaN(+d["RAM(GB)"]) &&
+d["Battery Capacity(mAh)"] >= bubbleThreshold
)

Insert cell
averageData = d3.rollups(
validData.filter(d => +d["Launched Price (USA)USD"] < 10000), // Remove price outliers
v => ({
Avg_Battery_mAh: d3.mean(v, d => +d["Battery Capacity(mAh)"]),
Avg_Launched_Price_USD: d3.mean(v, d => +d["Launched Price (USA)USD"])
}),
d => +d["RAM(GB)"]
).map(([ram, values]) => ({
RAM_GB: ram,
Avg_Battery_mAh: values.Avg_Battery_mAh,
Avg_Launched_Price_USD: values.Avg_Launched_Price_USD
}))

Insert cell
Insert cell
viewof bubbleThreshold = Inputs.range([6000, 11000], {
step: 100,
value: 1000, // lower starting point
label: "Battery (mAh)"
})

Insert cell
Plot.plot({
title: "Distribution of Smartphone Prices by Battery Capacity and RAM",
grid: true,
width: 800,
height: 500,
x: {
label: "Battery Capacity (mAh)",
domain: [5800, 11200] // horizontal space added
},
y: {
label: "Launched Price (USD)",
tickFormat: d => `$${d.toLocaleString()}`,
domain: [100, 1850] // adjusted to prevent bubble overflow
},
color: {
label: "RAM (GB)",
type: "linear",
scheme: "viridis",
legend: true
},
marks: [
Plot.dot(validData.filter(d => {
const price = +d["Launched Price (USA)USD"];
const battery = +d["Battery Capacity(mAh)"];
const ram = +d["RAM(GB)"];
return (
!isNaN(price) && price < 10000 &&
!isNaN(battery) && battery >= 6000 && battery <= 11000 &&
!isNaN(ram)
);
}), {
x: d => +d["Battery Capacity(mAh)"],
y: d => +d["Launched Price (USA)USD"],
fill: d => +d["RAM(GB)"],
stroke: d => +d["RAM(GB)"],
r: 4,
fillOpacity: 0.7,
strokeOpacity: 0.9,
title: d => `RAM: ${d["RAM(GB)"]} GB\nBattery: ${d["Battery Capacity(mAh)"]} mAh\nPrice: $${d["Launched Price (USA)USD"]}`,
clip: true // prevents any accidental overflow
})
]
})

Insert cell
d3.rollups(
data,
v => ({
avgPrice: d3.mean(v, d => +d["Launched Price (USA)USD"]),
avgWeight: d3.mean(v, d => +d["Mobile Weight(g)"])
}),
d => d["Company Name"]
)

Insert cell
topCompanies = {
const grouped = d3.rollups(
data,
v => ({
avgPrice: d3.mean(v, d => +d["Launched Price (USA)USD"]),
avgWeight: d3.mean(v, d => +d["Mobile Weight(g)"])
}),
d => d["Company Name"]
);

return grouped
.map(([company, values]) => ({
company,
avgPrice: values.avgPrice,
avgWeight: values.avgWeight
}))
.sort((a, b) => d3.descending(a.avgPrice, b.avgPrice))
.slice(0, 10);
}

Insert cell
import {select} from "@jashkenas/inputs"

Insert cell
filteredData = data.filter(d => String(d["Model Name"]).trim() === selectedModel)

Insert cell
filteredData

Insert cell
topCompaniesFiltered = d3.rollups(
data,
v => ({
avgPrice: d3.mean(v, d => +d["Launched Price (USA)USD"]),
avgWeight: d3.mean(v, d => +d["Mobile Weight(g)"])
}),
d => d["Company Name"]
)
.map(([company, stats]) => ({
company,
avgPrice: stats.avgPrice,
avgWeight: stats.avgWeight
}))
.filter(d => d.avgPrice && d.avgWeight) // Ensure no nulls
.sort((a, b) => d3.descending(a.avgPrice, b.avgPrice))
.slice(0, 10)

Insert cell
selectedEntry = data.find(d => String(d["Model Name"]).trim() === selectedModel)

Insert cell
filteredSubset = data.filter(d => d["Operating System"] === selectedEntry["Operating System"])

Insert cell
top10Models = {
return data
.map(d => ({
model: String(d["Model Name"] || "").trim(),
price: +d["Launched Price (USA)USD"]
}))
.filter(d => d.model && d.price) // remove rows with missing values
.sort((a, b) => b.price - a.price)
.slice(0, 10)
.map(d => d.model);
}

Insert cell
Insert cell
viewof selectedModel = Inputs.select(
top10Models,
{ label: "Select a Top 10 Mobile Models" }
)

Insert cell
top10Chart = {
if (!selectedEntry) return;

const sortedData = topCompaniesFiltered
.slice()
.sort((a, b) => b.avgPrice - a.avgPrice);

const companyOrder = sortedData.map(d => d.company);

return Plot.plot({
title: `Top 10 Mobile Companies and brands by Average Launch Price (Model: ${selectedEntry["Model Name"]})`,
width: 850,
height: 600,
marginLeft: 140,
marginRight: 40,
x: {
label: "Average Launch Price (USD)",
grid: true,
tickFormat: d => `$${d}`,
domain: [0, 4000]
},
y: {
label: "Company",
domain: companyOrder
},
color: {
legend: false
},
marks: [
Plot.barX(sortedData, {
x: "avgPrice",
y: "company",
fill: d =>
d.company === selectedEntry["Company Name"]
? "#1e3a8a"
: "#90caf9",
title: d => `${d.company}
Avg Price: $${d.avgPrice.toFixed(2)}
Avg Weight: ${d.avgWeight.toFixed(0)}g`
}),
Plot.ruleX([0])
]
});
}

Insert cell
mutable activeModel = null

Insert cell
mutable barClickEntry = null

Insert cell
updateModel = d => {
mutable activeModel = d
mutable barClickEntry = d
}

Insert cell
activeModel
Insert cell
mutable selecteddCompany = null
Insert cell
mutable selectedYear = null
Insert cell
mutable hoveredModel = null

Insert cell
mutable selecteddmodel = null;
Insert cell
hoveredModel; // <- this makes the cell reactive to hover updates

Insert cell
viewof ramChart = {
const model =
activeModel ??
(selectedModel && filteredData.find(d => d.Model === selectedModel)) ??
null;

const hovered = hoveredModel && filteredData.find(d => d.Model === hoveredModel);
const hoveredCompany = hovered?.company;
const hoveredYear = hovered?.year;

const data = averageRAM.filter(d => !model || d.company === model.company);

const chart = Plot.plot({
width: 400,
height: 400,
grid: true,
x: { label: "Launched Year", type: "band" },
y: { label: "Average RAM (GB)", nice: true },
color: {
legend: true,
label: "Company",
domain: Array.from(new Set(averageRAM.map(d => d.company))),
scheme: "category10"
},
marks: [
Plot.barY(data, {
x: "year",
y: "avgRam",
fill: "company",
title: d =>
d?.company
? `${d.company} (${d.year})\nAvg RAM: ${d.avgRam?.toFixed(2)} GB`
: null
})
]
});

const bars = chart.querySelectorAll("rect");

bars.forEach((bar, i) => {
const d = data[i];
bar.style.cursor = "pointer";
bar.__data__ = d;

// Reset styles
bar.style.stroke = "none";
bar.style.strokeWidth = "0";

// Highlight if selected
if (
d &&
model &&
d.company === model.company &&
d.year === model.year
) {
bar.style.fill = "#4caf50"; // selected
}

// Highlight if hovered
else if (
d &&
hoveredCompany === d.company &&
hoveredYear === d.year
) {
bar.style.stroke = "black";
bar.style.strokeWidth = "2";
}

// Single click: toggle selection
bar.addEventListener("click", () => {
if (
activeModel &&
activeModel.company === d.company &&
activeModel.year === d.year
) {
mutable activeModel = null;
mutable selectedRam = null;
} else {
mutable activeModel = d;
mutable selectedRam = null;
}
});

// Double click: reset selection
bar.addEventListener("dblclick", () => {
mutable activeModel = null;
mutable selectedRam = null;
});
});

return chart;
}

Insert cell
mutable activeCompany = null

Insert cell
activeModel
Insert cell
hoveredModel;



Insert cell
selectedRam;
Insert cell
mutable selectedRam = null
Insert cell
mutable selecteddddModel = null
Insert cell
viewof bubbleChart = {
const selected = selectedRam;
const model = activeModel;

const data = validData.filter(d => {
const price = +d["Launched Price (USA)USD"];
const battery = +d["Battery Capacity(mAh)"];
const ram = +d["RAM(GB)"];
const company = d["Company Name"];

return (
!isNaN(price) &&
price < 10000 &&
!isNaN(battery) &&
battery >= 5500 &&
battery <= 11000 &&
!isNaN(ram) &&
(!model || company === model.company)
);
});

const title = html`<h3 style="text-align:center; margin-bottom: 0.5em;">
</h3>`;

const chart = Plot.plot({
width: 800,
height: 600,
grid: true,
inset: 10,
x: {
label: "Battery Capacity (mAh)",
domain: [5500, 11200],
tickFormat: d => d.toLocaleString()
},
y: {
label: "Launched Price (USD)",
domain: [100, 1950],
tickFormat: d => `$${d.toLocaleString()}`
},
color: {
label: "RAM (GB)",
type: "linear",
scheme: "viridis",
legend: true
},
marks: [
Plot.dot(data, {
x: d => +d["Battery Capacity(mAh)"],
y: d => +d["Launched Price (USA)USD"] + 100,
fill: d => +d["RAM(GB)"],
stroke: d => +d["RAM(GB)"],
r: 5,
fillOpacity: d =>
selected === null
? 0.7
: +d["RAM(GB)"] === selected
? 1
: 0.1,
strokeOpacity: d =>
selected === null
? 0.9
: +d["RAM(GB)"] === selected
? 1
: 0.1,
title: d =>
`Model: ${d["Model Name"]}\nRAM: ${d["RAM(GB)"]} GB\nBattery: ${d["Battery Capacity(mAh)"]} mAh\nPrice: $${d["Launched Price (USA)USD"]}`,
clip: true
})
]
});

const wrapper = html`<div></div>`;
wrapper.append(title, chart);

const dots = chart.querySelectorAll("circle");
dots.forEach((dot, i) => {
const d = data[i];
if (!d) return;

const ramValue = +d["RAM(GB)"];
dot.style.cursor = "pointer";
dot.__data__ = d;

dot.addEventListener("click", () => {
// 🟡 NEW: select the model too
mutable selecteddddModel = d["Model Name"];

if (selectedRam === ramValue) {
mutable selectedRam = null;
} else {
mutable selectedRam = ramValue;
mutable activeModel = null;
}
});
});

return wrapper;
}

Insert cell
viewof top10byprice = {
// Ensure dependencies are tracked
selecteddModel;
activeModel;

const sortedData = topCompaniesFiltered
.slice()
.sort((a, b) => b.avgPrice - a.avgPrice);

const companyOrder = sortedData.map(d => d.company);

const activeCompany = activeModel?.company;

const selectedCompany = selectedModel && data.find(d => d["Model Name"] === selecteddModel)?.["Company Name"];

const chart = Plot.plot({
width: 850,
height: 600,
marginLeft: 140,
marginRight: 40,
x: {
label: "Average Launch Price (USD)",
grid: true,
tickFormat: d => `$${d.toLocaleString()}`,
domain: [0, 4100]
},
y: {
label: "Company",
domain: companyOrder
},
color: { legend: false },
marks: [
// Both clicked & selected
Plot.barX(
sortedData.filter(d => d.company === activeCompany && d.company === selectedCompany),
{
x: d => d.avgPrice + 100,
y: "company",
fill: "#0d9488"
}
),
// Selected only
Plot.barX(
sortedData.filter(d => d.company === selectedCompany && d.company !== activeCompany),
{
x: d => d.avgPrice + 100,
y: "company",
fill: "#1e3a8a"
}
),
// Clicked only
Plot.barX(
sortedData.filter(d => d.company === activeCompany && d.company !== selectedCompany),
{
x: d => d.avgPrice + 100,
y: "company",
fill: "#4caf50"
}
),
// Default
Plot.barX(
sortedData.filter(d => d.company !== selectedCompany && d.company !== activeCompany),
{
x: d => d.avgPrice + 100,
y: "company",
fill: "#90caf9"
}
),
Plot.ruleX([0])
]
});

// Add interactivity
chart.querySelectorAll("rect").forEach((bar, i) => {
const d = sortedData[i];
const company = d.company;

bar.style.cursor = "pointer";

let clickCount = 0;
let timer;

bar.addEventListener("click", () => {
clickCount++;

if (clickCount === 1) {
// Wait for possible double click
timer = setTimeout(() => {
// Single click
if (activeModel?.company === company) {
// Do nothing on single click if already selected
} else {
const recentModel = validData
.filter(e => e["Company Name"] === company)
.sort((a, b) => +b["Launched Year"] - +a["Launched Year"])[0];

if (recentModel) {
mutable activeModel = {
company: recentModel["Company Name"],
model: recentModel["Model Name"],
year: recentModel["Launched Year"]
};
} else {
mutable activeeModel = { company };
}
}
clickCount = 0;
}, 250);
} else {
// Double click
clearTimeout(timer);
mutable activeModel = null;
clickCount = 0;
}
});
});

return chart;
}

Insert cell
mutable activeeModel = null
Insert cell
activeeModel
Insert cell
selecteddEntry = data.find(d => String(d["Model Name"]).trim() === selecteddModel)

Insert cell
selecteddentry = null
Insert cell
Object.keys(data[0])

Insert cell
viewof selecteddModel = Inputs.select(
top10Models, // array of model names
{ label: "Select a Top 10 Mobile Model" }
)

Insert cell
viewof dashboard = {
const container = html`<div style="max-width: 1200px; margin: 0 auto; padding: 2rem;"></div>`;

const style = html`<style>
.dashboard-title-card,
.chart-card {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
margin-bottom: 2rem;
}

.dashboard-title {
font-size: 1.4rem;
font-weight: 600;
text-align: center;
}

.dashboard-title a {
color: #111;
text-decoration: none;
}

.dashboard-title a:hover {
text-decoration: underline;
}

.charts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}

.chart-full {
grid-column: span 2;
}

.chart-title {
font-size: 1.1rem;
font-weight: 600;
text-align: center;
margin-bottom: 1rem;
}

.control-wrapper {
margin-bottom: 1rem;
text-align: center;
}
</style>`;

const titleCard = html`<div class="dashboard-title-card"></div>`;
const title = html`<div class="dashboard-title"></div>`;
const link = html`<a href="https://www.kaggle.com/datasets/abdulmalik1518/mobiles-dataset-2025" target="_blank">📱 Smartphone Market Overview and Brand Insights (2025)</a>`;
title.appendChild(link);
titleCard.appendChild(title);

const chartsGrid = html`<div class="charts-grid"></div>`;

const chart1 = html`<div class="chart-card">
<div class="chart-title">📊 Comparison of RAM by Smartphone Brand Over Time</div>
${viewof ramChart}
</div>`;

const chart2 = html`<div class="chart-card">
<div class="control-wrapper">${viewof bubbleThreshold}</div>
<div class="chart-title">🔋 Distribution of Smartphone Prices by Battery Capacity and RAM</div>
${viewof bubbleChart}
</div>`;

const chart3 = html`<div class="chart-card chart-full">
<div class="control-wrapper">${viewof selecteddModel}</div>
<div class="chart-title">🏷️ Top 10 Mobile Companies and Brands by Average Launch Price (Model: ${selectedEntry["Model Name"]})</div>
${viewof top10byprice}
</div>`;

chartsGrid.append(chart1, chart2, chart3);
container.append(style, titleCard, chartsGrid);
return container;
}

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