This was originally published on codinhood.com
Doomsayers have been declaring the death of Apple for decades. Apple is not unique in this regard, of course. Every company has had its share of dedicated detractors announcing their demise. Blockbuster's end was predicated for years before it formally announced bankruptcy, while Bitcoin has been declared dead over 380 times since 2010. Some harbingers were right, some were wrong. This article is not here to pass judgment on those predictions or prophesize the future of Apple but simply to visualize some fun data points using Javascript, React, and Nivo Charts.
If you're only interested in the charts themselves and not how they were made then checkout the Apple Doomsayers Demo Page.
Apple Death Knell Data
The data is sourced from The Mac Observer's excellent Apple Death Knell Counter, which is a collection of predictions from public figures, mostly journalists, about the end of Apple. The Mac Observer staff curates the list to include only instances where the speaker is specifically stating the demise and doom of Apple and not include a simply negative sentiment.
Articles where Apple is referred to as "beleaguered" are not necessarily eligible, and in fact, the vast majority of such references are simply colorfully negative descriptions that do not qualify as an Apple Death Knell. - The Mac Observer
Unfortunately, some of the earliest death knells no longer link to live web pages. A few of these can be found on the Wayback Machine, but others cannot. For visualization purposes, we're just going to trust that all accounts are accurate.
I scraped the data from MacObserver using Beautiful Soup to scrape the Title
, Author
, Date
, Source
, and Publisher
for each entry.
This gives us an array of 71 objects where each object represents one predication. From this data alone we can create our first graph. Rather than build these charts from scratch, we're going to use Nivo, which provides a set of highly customizable React components built on D3 for visualizing data. We're also going to use Day.js for dealing with dates and times.
Deaths Per Year - Data Transform
What we want to display is the number of deaths per year, which means we need to transform our array of deaths into an array of objects with two keys, one for the year of deaths and one for the number of deaths. Since Nivo requires data along the x-axis
to be named x
and data along the y-axis
to be named y
, we'll add the year to x
and number of deaths to y
.
function calculateYearsBetween(startYear) {
let currentYear = new Date().getFullYear();
let years = [];
startYear = startYear;
while (startYear <= currentYear) {
years.push({ x: startYear, y: 0 });
startYear++;
}
return years;
}
Next, create a function that loops through the death array and increments the correct year in the yearArray
. Nivo again requires a special object format that includes a unique id
to name the chart and data
property to contain the actual data we want to visualize, which is the yearArray
.
function deathsPerYear() {
const yearArray = calculateYearsBetween(1994);
appleDeaths.forEach((death, index) => {
const dayjsDate = dayjs(death.Date);
const yearOfDeath = dayjsDate.get("year");
const inYearArray = yearArray.findIndex(year => {
return year.year == yearOfDeath;
});
yearArray[inYearArray].y++;
});
const formattedData = [{ id: "deaths", data: yearArray }];
return formattedData;
}
The result of the two functions above is an array that looks like this:
[
id: 'deaths',
data: [
{x: 1994: y: 0},
{x: 1995: y: 1},
{x: 1996: y: 2},
{x: 1997: y: 7},
...
]
]
Deaths Per Year - Charts with Nivo
Using Nivo's Line chart we can graph the above data for each year. Although Nivo charts have props for practically every part of the graph, all we need to get started is a defined height and width. We'll also define a container with overflow: auto
so we can see the full chart on mobile by scrolling.
import { Line } from "@nivo/line";
import React from "react";
import { deathsPerYear } from "./data";
const DeathsPerYearGraph = () => {
const newData = deathsPerYear();
return (
<div style={styles.container}>
<Line data={newData} margin width={780} height={500} />
</div>
);
};
const styles = {
container: {
height: 500,
maxWidth: 780,
overflow: "auto",
},
};
export { DeathsPerYearGraph };
Nivo Chart Margins
The first problem is that there is no margin between the graph itself and the edge of the container, which means the row and column labels are hidden. The margin
prop allows us to define this margin and reveal the labels.
...
<Line data={newData} width={780} height={500} margin={styles.chartMargin} />
...
chartMargin: {
top: 50,
right: 50,
bottom: 50,
left: 60,
},
...
Nivo Axis Legends and Grid Lines
By default, Nivo charts do not have axis legends, but we can add a legend to any side of the chart using axisBottom
, axisLeft
, axisRight
, and axisTop
props. These props take in an object with various properties that allow us to, among other things, add legends, position legends, define axis tick size, padding, and rotation.
We can also remove the grid lines, which I think are distracting, by passing false
to both the enableGridX
and enableGridY
prop.
...
<Line
data={newData}
width={780}
height={500}
margin={styles.chartMargin}
enableGridX={false}
enableGridY={false}
axisBottom={styles.axisBottom}
axisLeft={styles.axisLeft}
/>
...
axisLeft: {
orient: "left",
tickSize: 5,
tickPadding: 5,
tickRotation: 0,
legend: "Apple Deaths",
legendOffset: -40,
legendPosition: "middle",
},
axisBottom: {
orient: "bottom",
tickSize: 5,
tickPadding: 5,
tickRotation: 0,
legend: "Year",
legendOffset: 36,
legendPosition: "middle",
},
...
Nivo Theme and Points
Out-of-the-box Nivo charts can utilize color schemes from the d3-scale-chromotic module, but we can define a completely custom theme by passing in an array of colors to the colors
prop. Passing in one color will define the color for the line, however, it will not define the color of the actual data points, that is, the circles on the line. To change the point size and color we can use the aptly named, pointSize
and pointColor
props.
Nivo charts also accept a theme
prop that will allow us to style the text color and text size to make it readable on dark backgrounds.
...
<Line
data={newData}
width={780}
height={500}
margin={styles.chartMargin}
enableGridX={false}
enableGridY={false}
axisBottom={styles.axisBottom}
axisLeft={styles.axisLeft}
colors={["#03e1e5"]}
theme={styles.theme}
pointSize={10}
pointColor={"#03e1e5"}
/>
...
theme: {
fontSize: "14px",
textColor: "#7b7b99",
},
...
Nivo X-Axis Scale
Now that the labels are much easier to read, you'll notice that the x-axis column names are overlapping. The chart is trying to show a label for every single data point along the x-axis, which is the default behavior for scale type point
. We can change the x-axis scale type with the property xScale
. In this case, we want to change the scale type to linear
, which will display labels evenly across a specific range. If do not provide that range, then the chart will start from 0 and show equal increments to our end date 2020. But we don't care about dates before 1994 (where our data starts), so we need to set a minimum date for the x-axis to start at, which we can do using the min
property.
<Line
data={newData}
width={780}
height={500}
margin={styles.chartMargin}
enableGridX={false}
enableGridY={false}
axisBottom={styles.axisBottom}
axisLeft={styles.axisLeft}
colors={["#03e1e5"]}
theme={styles.theme}
pointSize={10}
pointColor={"#03e1e5"}
xScale={{ type: "linear", min: "1994" }}
/>
Nivo Hover Labels
Adding the useMesh
prop will display a label next to each data point when you hover over it with the values for that data pont. To customize this hover label, however, we need to provide our own label component. First, create a custom label component, Tooltip, which takes slice
as a prop. Nivo will pass each data point (slice) to this component with other useful information about the chart to create a custom label.
const Tooltip = function({ slice }) {
return (
<div
style={{
background: "#09001b",
padding: "9px 12px",
}}
>
{slice.points.map(point => (
<div
key={point.id}
style={{
color: point.serieColor,
padding: "3px 0",
}}
>
<strong>{point.serieId}</strong> {point.data.yFormatted}
</div>
))}
</div>
);
};
Now we can pass this custom Tooltip into the sliceTooltip
prop with the slice
prop. Also, enable custom tooltips (hover labels) by providing the enableSlices
prop with the string, 'x'
<Line
data={newData}
width={780}
height={500}
margin={styles.chartMargin}
enableGridX={false}
enableGridY={false}
axisBottom={styles.axisBottom}
axisLeft={styles.axisLeft}
colors={["#03e1e5"]}
theme={styles.theme}
pointSize={10}
pointColor={"#03e1e5"}
xScale={{ type: "linear", min: "1994" }}
enableSlices="x"
sliceTooltip={({ slice }) => {
return <Tooltip slice={slice} />;
}}
/>
Now when you hover over the chart, a tooltip will display the number of deaths.
Nivo Area Chart
We can easily convert this line chart into an Area chart by adding the enableArea
prop.
<Line
data={newData}
width={780}
height={500}
margin={styles.chartMargin}
xScale={{ type: "linear", min: "1994" }}
enableGridX={false}
enableGridY={false}
axisBottom={styles.axisBottom}
axisLeft={styles.axisLeft}
colors={["#03e1e5"]}
pointSize={10}
pointColor={"#03e1e5"}
theme={styles.theme}
enableSlices="x"
sliceTooltip={({ slice }) => {
return <Tooltip slice={slice} />;
}}
enableArea={true}
/>
Nivo Highlight Markers
The last things we're going to add to this line chart are markers to highlight specific events in Apple's history on the chart to give more context. Nivo allows us to create vertical or horizontal lines with labels at any point on the chart by passing an array of objects to the markers
prop. Each object in that array is a separate marker with properties that define which axis it should display along, the point or value to display, the style of the marker, and the text, if any, to show. Let's create three markers, one for the introduction of the iPod, the introduction of the iPhone, and Steven Job's death.
const contextLines = [
{
axis: "x",
value: 2011,
lineStyle: { stroke: "#09646b", strokeWidth: 2 },
legend: "Steven Jobs' Death",
textStyle: {
fill: "7b7b99",
},
},
{
axis: "x",
value: 2007,
lineStyle: { stroke: "#09646b", strokeWidth: 2 },
legend: "iPhone",
textStyle: {
fill: "7b7b99",
},
},
{
axis: "x",
value: 2001,
lineStyle: { stroke: "#09646b", strokeWidth: 2 },
legend: "iPod",
textStyle: {
fill: "7b7b99",
},
orient: "bottom",
legendPosition: "top-left",
},
];
<Line
data={newData}
width={780}
height={500}
margin={styles.chartMargin}
xScale={{ type: "linear", min: "1994" }}
enableGridX={false}
enableGridY={false}
axisBottom={styles.axisBottom}
axisLeft={styles.axisLeft}
colors={["#03e1e5"]}
pointSize={10}
pointColor={"#03e1e5"}
theme={styles.theme}
enableSlices="x"
sliceTooltip={({ slice }) => {
return <Tooltip slice={slice} />;
}}
enableArea={true}
markers={contextLines}
/>
Deaths Per Author - Data Transform
The next chart will be a pie graph displaying the number of death predictions per author. First, similarly to the deaths per year data, we need to transform the death array into an array that shows deaths per author. The Nivo pie chart expects the data to be an array of objects with each object containing an id
and value
. Creating this array will reveal that the vast majority of predictions were made by different people. To avoid graphing 50+ data points with only 1 value, we will filter the results for authors that have more than one prediction on our list using the filter method. Finally, we only want named authors so we will also filter out all "Unknow" authors.
export function deathsPerAuthor() {
const perAuthorArray = [];
appleDeaths.forEach((death, index) => {
if (index == 0) {
perAuthorArray.push({ id: death.Author, value: 1 });
}
const inPerAuthorArray = perAuthorArray.findIndex(author => {
return author.id == death.Author;
});
if (inPerAuthorArray > -1) {
perAuthorArray[inPerAuthorArray].value++;
} else {
perAuthorArray.push({ id: death.Author, value: 1 });
}
});
const filtered = perAuthorArray.filter(author => author.value > 1);
return filtered;
}
The data will end up looking like this:
[
{
id: "Michael Dell",
value: 2,
},
...
];
Deaths Per Author - Nivo Pie Chart
We can create a simple Pie
chart using the data above in a similar way to the line chart above. Remember, we need to set margins within the chart so nothing will get cut off. Let's also set the scheme to set2
.
import { Pie } from "@nivo/pie";
import React from "react";
import { deathsPerAuthor } from "./data";
const DeathsPerAuthor = ({ version }) => {
const newData = deathsPerAuthor();
return (
<div style={styles.container}>
<Pie
data={newData}
width={780}
height={500}
margin={styles.chartMargin}
colors={{ scheme: "set2" }}
/>
</div>
);
};
const styles = {
container: {
height: 500,
maxWidth: 780,
background: "#09001b",
overflow: "auto",
},
chartMargin: {
top: 40,
right: 80,
bottom: 40,
left: 80,
},
};
export { DeathsPerAuthor };
Nivo Donut Chart and Section Styles
Nivo allows us to create a donut chart by defining the size of the inner radius using the innerRadius
prop, try playing around with this prop on Nivo's interactive documentation for pie charts. We can add some padding in between each data section using the padAngle
prop, which will make it easier to distinguish each section. The cornerRadius
prop defines the radius of each section of the pie.
<Pie
data={newData}
width={780}
height={500}
margin={styles.chartMargin}
colors={{ scheme: "set2" }}
animate={true}
innerRadius={0.5}
padAngle={0.7}
cornerRadius={3}
borderWidth={1}
borderColor={{ from: "color", modifiers: [["darker", 0.2]] }}
/>
Nivo Radial Labels
The chart labels are hard to read on dark backgrounds, luckily Nivo gives ample customization of these labels. We can change the label color with the radialLabelsTextColor
prop. The radialLabelsLinkDiagonalLength
and radialLabelsLinkHorizontalLength
props allow us to customize the exact length of each part of the line to the label, while radialLabelsLinkStrokeWidth
defines the line's width. The radialLabelsLinkColor
defines the color of the line, setting this to from: "color"
will make the line match the color of the section it's coming from. Finally, we can also customize the spacing between the line, label, and data section, but I think the defaults are fine here.
<Pie
data={newData}
width={780}
height={500}
margin={styles.chartMargin}
colors={{ scheme: "set2" }}
animate={true}
innerRadius={0.5}
padAngle={0.7}
cornerRadius={3}
radialLabelsTextColor="#7b7b99"
radialLabelsLinkDiagonalLength={16}
radialLabelsLinkHorizontalLength={24}
radialLabelsLinkStrokeWidth={1.3}
radialLabelsLinkColor={{ from: "color" }}
/>
Nivo Legends
Nivo legends are available for each chart type and are defined as an array of objects on the legends
prop. The legend position, in relation to the chart itself, is defined by the anchor
property, for this chart let's define it at the bottom. The direction
prop can either be a row
or a column
. Each legend item can further be customized with the specific props itemWidth
, itemWidth
, and itemTextColor
. The symbol that appears next to the text can either be a circle, triangle, square, or diamond. Finally, we need to change the bottom chart margins to give room for this legend.
<Pie
data={newData}
width={780}
height={500}
margin={{ top: 40, right: 80, bottom: 80, left: 80 }}
colors={{ scheme: "set2" }}
animate={true}
innerRadius={0.5}
padAngle={0.7}
cornerRadius={3}
radialLabelsTextColor="#7b7b99"
radialLabelsLinkDiagonalLength={16}
radialLabelsLinkHorizontalLength={24}
radialLabelsLinkStrokeWidth={1}
radialLabelsLinkColor={{ from: "color" }}
legends={[
{
anchor: "bottom",
direction: "row",
translateY: 56,
itemWidth: 120,
itemHeight: 18,
itemTextColor: "#999",
symbolSize: 18,
symbolShape: "circle",
},
]}
/>
Deaths Per Publication - Data Transform
This function is almost identical to the per author function except we're looking for publications that have multiple death knells, instead of authors. We could create a shared function that accepts a property to filter for, but we can just as easily copy and paste for now.
export function deathsPerPublication() {
const perPubArray = [];
appleDeaths.forEach((death, index) => {
if (index == 0) {
perPubArray.push({ id: death.Publisher, value: 1 });
}
const inPerAuthorArray = perPubArray.findIndex(author => {
return author.id == death.Publisher;
});
if (inPerAuthorArray > -1) {
perPubArray[inPerAuthorArray].value++;
} else {
perPubArray.push({ id: death.Publisher, value: 1 });
}
});
const filtered = perPubArray.filter(author => {
const isAboveOne = author.value > 1;
const isNotUnknown = author.id !== "Unknown";
return isAboveOne && isNotUnknown;
});
return filtered;
}
The data will end up looking like this:
[
{
id: "Mac Observer",
value: 2,
},
...
];
Deaths Per Publication - Pie Chart
The data here is so similar to the per author data that we're going to resuse the pie chart we created above and simply provide this publication data.
import { Pie } from "@nivo/pie";
import React from "react";
import { deathsPerPublication } from "./data";
const DeathsPerPublication = ({ version }) => {
const newData = deathsPerPublication();
let chart = (
<Pie
data={newData}
width={780}
height={500}
margin={styles.chartMargin}
colors={{ scheme: "set2" }}
animate={true}
innerRadius={0.5}
padAngle={0.7}
cornerRadius={3}
radialLabelsTextColor="#7b7b99"
radialLabelsLinkDiagonalLength={16}
radialLabelsLinkHorizontalLength={24}
radialLabelsLinkStrokeWidth={1}
radialLabelsLinkColor={{ from: "color" }}
/>
);
return <div style={styles.container}>{chart}</div>;
};
const styles = {
container: {
height: 500,
maxWidth: 780,
background: "#09001b",
},
chartMargin: {
top: 40,
right: 80,
bottom: 40,
left: 80,
},
};
export { DeathsPerPublication };
Word cloud
For fun, I generated a word cloud using wordclouds.com with all the relevant death knell quotes.
Conclusion
Nivo charts contain a lot of functionality out-of-the-box while allowing developers to customize almost all aspects of a chart. However, there are numerous other charting libraries for React and Javascript, check out How to Build a Bitcoin DCA Chart with React and Recharts to see how Recharts differs from Nivo.
Top comments (0)