HTMLcopy
x
1
<button class="zoom-button" data-zoom-level="kingdom">Roman Kingdom</button>
2
<button class="zoom-button" data-zoom-level="republic">Roman Republic</button>
3
<button class="zoom-button" data-zoom-level="empire">Roman Empire</button>
4
<button class="zoom-button" data-zoom-level="empires">Two Empires</button>
5
<button class="zoom-button" data-zoom-level="out">Zoom out</button>
6
7
<div id="container"></div>
CSScopy
48
1
html, body {
2
width: 100%;
3
height: 100%;
4
margin: 0;
5
padding: 0;
6
}
7
8
#container {
9
position: absolute;
10
min-width: 1000px;
11
width: 100%;
12
top: 40px;
13
bottom: 0;
14
}
15
16
.zoom-button {
17
background-color: #bad5f1;
18
border: none;
19
padding: 5px 10px;
20
text-align: center;
21
text-decoration: none;
22
display: inline-block;
23
font-size: 16px;
24
font-family: "Cinzel", serif;
25
margin-top: 5px;
26
margin-left: 5px;
27
}
28
29
.zoom-button:hover {
30
color: #fff;
31
background-color: #1976d2;
32
}
33
34
.zoom-button:last-of-type {
35
position: absolute;
36
right: 5px;
37
}
38
39
img {
40
max-width: 300px;
41
max-height: 300px;
42
}
43
44
p {
45
font-family: "Cinzel", serif;
46
margin-top: 1px;
47
margin-bottom: 1px;
48
}
JavaScriptcopy
296
1
// ES3 polyfill for structuredClone
2
if (typeof structuredClone !== 'function') {
3
structuredClone = function(obj) { // eslint-disable-line no-undef
4
function clone(obj) {
5
var copy;
6
7
// Handle the 3 simple types, and null or undefined
8
if (obj === null || typeof obj !== 'object') return obj;
9
10
// Handle Date
11
if (obj instanceof Date) {
12
copy = new Date();
13
copy.setTime(obj.getTime());
14
return copy;
15
}
16
17
// Handle Array
18
if (obj instanceof Array) {
19
copy = [];
20
for (var i = 0, len = obj.length; i < len; i++) {
21
copy[i] = clone(obj[i]);
22
}
23
return copy;
24
}
25
26
// Handle Object
27
if (obj instanceof Object) {
28
copy = {};
29
for (var attr in obj) {
30
if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]);
31
}
32
return copy;
33
}
34
35
throw new Error('Unable to copy obj! Its type isn\'t supported.');
36
}
37
38
return clone(obj);
39
};
40
}
41
42
// Create a chart variable here to be accessible from various places in the script
43
var chart;
44
45
// Ensure the main charting code runs after the page has loaded with all external resources
46
anychart.onDocumentReady(function () {
47
// Create a timeline chart
48
chart = anychart.timeline();
49
var tooltip;
50
51
// Set the minimum and maximum dates of the timeline scale
52
chart.scale().minimum(Date.UTC(-1000));
53
chart.scale().maximum(Date.UTC(1500));
54
55
// Set the chart's data
56
// loadData function is defined in
57
// https://cdn.anychart.com/samples-data/timeline-charts/roman-history-timeline/data.js
58
var data = loadData(); // eslint-disable-line no-undef
59
60
// 1) Set the range series for periods (e.g., rulers, empires)
61
for (var i = 0; i < data.periods.length; i++) {
62
var period = data.periods[i];
63
var range = chart.range(period.ranges);
64
range.name(period.name);
65
range.direction(period.direction);
66
range.labels().fontFamily('Cinzel');
67
// Customize the range series tooltip:
68
tooltip = range.tooltip();
69
// enable HTML in the tooltip
70
tooltip.useHtml(true);
71
// format the tooltip content
72
tooltip.format(function() {
73
var img = this.getData('img');
74
var description = this.getData('description');
75
return '<center><img src=\'' + img + '\'/></center><p style=\'max-width: 250px;\'>' + description + '</p>';
76
});
77
// disable the tooltip separator
78
tooltip.separator(false);
79
// enable HTML in the tooltip header
80
tooltip.title().useHtml(true);
81
// format the tooltip header
82
tooltip.titleFormat(function() {
83
// use the anychart.format.dateTime() method to format the dates
84
var start = anychart.format.dateTime(this.start, 'y G');
85
var end = anychart.format.dateTime(this.end, 'y G');
86
return '<p>' + this.x + ' (' + start + ' – ' + end + ')</p>';
87
});
88
}
89
90
// 2) Set the moment series for events (e.g., battles, establishment of states)
91
for (var j = 0; j < data.events.length; j++) {
92
var event = data.events[j];
93
var moment = chart.moment(event.moments);
94
moment.name(event.name);
95
moment.direction(event.direction);
96
moment.labels().fontFamily('Cinzel');
97
// Configure the moment series tooltip:
98
tooltip = moment.tooltip();
99
// enable HTML in the tooltip
100
tooltip.useHtml(true);
101
// format the tooltip content
102
tooltip.format(function() {
103
var img = this.getData('img');
104
var description = this.getData('description');
105
return '<center><img src=\'' + img + '\'/></center><p style= \'max-width: 250px;\'>' + description + '</p>';
106
});
107
// disable the tooltip separator
108
tooltip.separator(false);
109
// enable HTML in the tooltip header
110
tooltip.title().useHtml(true);
111
// format the tooltip header
112
tooltip.titleFormat(function() {
113
var x = new Date(this.x);
114
// use the anychart.format.dateTime() method for date formatting
115
var formatted = anychart.format.dateTime(x, 'y G');
116
return '<p>' + this.value + ', ' + formatted + '</p>';
117
});
118
}
119
120
// Enable the scroller for better navigation of the timeline
121
chart.scroller(true);
122
123
// Add an event listener to zoom into an element when it's clicked:
124
chart.listen('pointClick', function(e) {
125
var start, end, gap;
126
// check if the clicked element is a moment or a period
127
var type = e.series.getType();
128
// configure the behavior for moments
129
if (type === 'moment') {
130
var momentDate = e.point.get('x');
131
// zoom into the moment, showing 50 years before and after it
132
start = momentDate;
133
end = momentDate;
134
// 50 years in milliseconds
135
gap = 1577880000000;
136
} else { // for periods (non-moment elements)
137
// zoom into the period, showing 10% of its length before its start and after its end
138
start = e.point.get('start');
139
end = e.point.get('end');
140
gap = (end - start) / 10;
141
}
142
// zoom into the element when it's clicked
143
chart.zoomTo(start - gap, end + gap);
144
});
145
146
// Patch the zoom levels since we're working with years and centuries (see lines 1745-1775)
147
chart.scale().zoomLevels(patchedZoomLevels());
148
149
// Disable the selection functionality to allow click tracking for zooming
150
chart.interactivity().selectionMode('none');
151
152
// Call the custom locale-altering function before the chart creation
153
// to tweak the locale to work with AD and BC (era designators) - see lines 1694-1743
154
patchDateTimeLocale();
155
156
// Customize the font of the axis labels
157
chart.axis().labels().fontFamily('Cinzel');
158
159
// Configure the chart's title
160
chart.title({
161
text: 'Roman Civilization',
162
fontFamily: 'Cinzel',
163
fontSize: 32
164
});
165
166
// Specify the chart container element by its id
167
chart.container('container');
168
169
// Draw the chart
170
chart.draw();
171
172
});
173
174
// Function to zoom into the target period when a button is clicked
175
function zoomTo(period) {
176
switch (period) {
177
case 'kingdom':
178
chart.zoomTo(Date.UTC(-755), Date.UTC(-505));
179
break;
180
case 'republic':
181
chart.zoomTo(Date.UTC(-515), Date.UTC(-20));
182
break;
183
case 'empire':
184
chart.zoomTo(Date.UTC(-40), Date.UTC(410));
185
break;
186
case 'empires':
187
chart.zoomTo(Date.UTC(380), Date.UTC(1460));
188
break;
189
default:
190
console.log('Invalid period');
191
}
192
}
193
194
// Function to zoom out, fitting the chart to the container
195
function zoomOut() {
196
chart.fit();
197
}
198
199
200
/*
201
There is a locale mechanism in AnyChart, which is described at https://docs.anychart.com/Common_Settings/Localization.
202
A sample locale can be seen here: https://cdn.anychart.com/releases/v8/locales/en-gb.js.
203
There is a block of keys that start with timeline_, which are used when the level of zoom changes.
204
To show Era designators, we can edit a locale's format property.
205
The era designator is also a property. In some locales, it is "AD" (Anno Domini) and "BC" (Before Christ),
206
while in others, it is "BCE" (Before Common Era) and "CE" (Common Era).
207
*/
208
209
// Custom function to modify locale formats to include an era designator:
210
function patchDateTimeLocale() {
211
// read the current output locale and create a structured clone of it
212
var currentOutputLocale = structuredClone(anychart.format.locales[anychart.format.outputLocale()]); // eslint-disable-line no-undef
213
// read the format object to prepare for creating a custom version of the locale
214
var dateTimeLocaleFormats = currentOutputLocale.dateTimeLocale.formats;
215
// loop through all available format keys to find those related to the current scale
216
for (var format in dateTimeLocaleFormats) {
217
// "timeline_" is the scale we are looking for
218
if (format.startsWith('timeline_')) {
219
// read and loop through an array of format values to insert an era designator
220
var formatValues = dateTimeLocaleFormats[format];
221
for (var i = 0; i < formatValues.length; i++) {
222
// formats are usually represented by strings like "yyyy" or "dd MMMM yy HH:mm.SSS"
223
var formatValue = formatValues[i];
224
// there are two possible configurations of year representation inside a locale string "yyyy" and "yy"
225
// we need to find both versions or neither to correctly modify the locale string
226
var index4Y = formatValue.search(/y{4}/g);
227
var index2Y = formatValue.search(/y{2}/g);
228
// if there is a string representing a "yyyy" year, modify the format to include an era designator
229
if (index4Y >= 0) {
230
/*
231
Create a new string that includes everything before and after the index identified earlier.
232
The new string will include an era designator, but ensure the indexes are adjusted correctly,
233
as we want to insert the era designator after the part of the string that represents the year.
234
*/
235
var formatValuePatched4Y = formatValue.slice(0, index4Y) + 'y G' + formatValue.slice(index4Y + 4);
236
formatValues[i] = formatValuePatched4Y;
237
// if there is a string representing a "yy" year, modify the format to include an era designator
238
} else if (index2Y > -1) {
239
var formatValuePatched2Y = formatValue.slice(0, index2Y + 1) + 'y G' + formatValue.slice(index2Y + 3);
240
formatValues[i] = formatValuePatched2Y;
241
}
242
}
243
}
244
}
245
// add the custom current output locale to AnyChart
246
anychart.format.locales['custom'] = currentOutputLocale;
247
// set the custom locale as the active locale
248
anychart.format.outputLocale('custom');
249
}
250
251
// Function to patch the zoom levels on the timeline scale
252
function patchedZoomLevels() {
253
var customZoomLevels = [
254
[
255
{'unit': 'month', 'count': 1},
256
{'unit': 'quarter', 'count': 1},
257
{'unit': 'year', 'count': 1}
258
],
259
[
260
{'unit': 'quarter', 'count': 1},
261
{'unit': 'year', 'count': 1},
262
{'unit': 'year', 'count': 10}
263
],
264
[
265
{'unit': 'year', 'count': 1},
266
{'unit': 'year', 'count': 10},
267
{'unit': 'year', 'count': 50}
268
],
269
[
270
{'unit': 'year', 'count': 10},
271
{'unit': 'year', 'count': 50},
272
{'unit': 'year', 'count': 100}
273
],
274
[
275
{'unit': 'year', 'count': 50},
276
{'unit': 'year', 'count': 100},
277
{'unit': 'year', 'count': 500}
278
]
279
];
280
return customZoomLevels;
281
}
282
283
// Add event listeners for zoom buttons
284
var zoomButtons = document.getElementsByClassName('zoom-button');
285
Array.from(zoomButtons).forEach(function (button) {
286
button.addEventListener('click', function() {
287
var zoomLevel = this.dataset.zoomLevel;
288
switch (zoomLevel) {
289
case 'out':
290
zoomOut();
291
break;
292
default:
293
zoomTo(zoomLevel);
294
}
295
});
296
});