@granite-elements/granite-timeline 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/README.md +150 -53
- package/granite-timeline.js +252 -706
- package/package.json +36 -19
- package/src/timeline-renderer.js +391 -0
- package/.eslintrc.json +0 -28
- package/demo/index.html +0 -50
- package/example.html +0 -367
- package/index.html +0 -16
- package/polymer.json +0 -7
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
2
|
+
"name": "@granite-elements/granite-timeline",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "A timeline rendering web component using Lit and d3",
|
|
3
5
|
"keywords": [
|
|
4
6
|
"web-component",
|
|
5
7
|
"web-components",
|
|
6
|
-
"
|
|
8
|
+
"lit",
|
|
7
9
|
"lostinbrittany",
|
|
8
10
|
"d3",
|
|
9
|
-
"d3-timelines",
|
|
10
11
|
"timeline"
|
|
11
12
|
],
|
|
12
13
|
"homepage": "https://github.com/LostInBrittany/granite-timeline",
|
|
@@ -14,26 +15,42 @@
|
|
|
14
15
|
"type": "git",
|
|
15
16
|
"url": "https://github.com/LostInBrittany/granite-timeline"
|
|
16
17
|
},
|
|
17
|
-
"name": "@granite-elements/granite-timeline",
|
|
18
|
-
"version": "2.0.0",
|
|
19
|
-
"resolutions": {
|
|
20
|
-
"inherits": "2.0.3",
|
|
21
|
-
"samsam": "1.1.3",
|
|
22
|
-
"supports-color": "3.1.2",
|
|
23
|
-
"type-detect": "1.0.0",
|
|
24
|
-
"@webcomponents/webcomponentsjs": "2.0.0-beta.2"
|
|
25
|
-
},
|
|
26
|
-
"main": "granite-timeline.js",
|
|
27
18
|
"author": "Horacio Gonzalez <horacio.gonzalez@gmail.com>",
|
|
28
19
|
"license": "MIT",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"main": "granite-timeline.js",
|
|
22
|
+
"module": "granite-timeline.js",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./granite-timeline.js",
|
|
25
|
+
"./granite-timeline.js": "./granite-timeline.js",
|
|
26
|
+
"./src/timeline-renderer.js": "./src/timeline-renderer.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"granite-timeline.js",
|
|
30
|
+
"src/",
|
|
31
|
+
"LICENSE.md",
|
|
32
|
+
"README.md",
|
|
33
|
+
"CHANGELOG.md"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"dev": "vite",
|
|
37
|
+
"build:demo": "vite build",
|
|
38
|
+
"lint": "eslint ."
|
|
39
|
+
},
|
|
29
40
|
"dependencies": {
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"d3-
|
|
41
|
+
"d3-axis": "^3.0.0",
|
|
42
|
+
"d3-scale": "^4.0.2",
|
|
43
|
+
"d3-scale-chromatic": "^3.1.0",
|
|
44
|
+
"d3-selection": "^3.0.0",
|
|
45
|
+
"d3-time": "^3.1.0",
|
|
46
|
+
"d3-time-format": "^4.1.0",
|
|
47
|
+
"d3-zoom": "^3.0.0",
|
|
48
|
+
"lit": "^3.2.0"
|
|
33
49
|
},
|
|
34
50
|
"devDependencies": {
|
|
35
|
-
"@
|
|
36
|
-
"
|
|
37
|
-
"
|
|
51
|
+
"@eslint/js": "^9.0.0",
|
|
52
|
+
"eslint": "^9.0.0",
|
|
53
|
+
"globals": "^15.0.0",
|
|
54
|
+
"vite": "^6.0.0"
|
|
38
55
|
}
|
|
39
56
|
}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure d3 v7 timeline renderer for `granite-timeline`.
|
|
3
|
+
*
|
|
4
|
+
* This module has no dependency on Lit or on the custom element: it renders
|
|
5
|
+
* a timeline SVG into any container element from a data array and a flat
|
|
6
|
+
* options object. It is a modern reimplementation of the rendering features
|
|
7
|
+
* of the (abandoned) `d3-timelines` plugin that granite-timeline v2 wrapped.
|
|
8
|
+
*
|
|
9
|
+
* Data format (unchanged from v2):
|
|
10
|
+
* ```
|
|
11
|
+
* [
|
|
12
|
+
* {
|
|
13
|
+
* label: 'series label', // optional, shown when stacked
|
|
14
|
+
* myColorProperty: 'apple', // optional, see colorsProperty
|
|
15
|
+
* times: [
|
|
16
|
+
* {
|
|
17
|
+
* starting_time: 1355752800000, // ms epoch
|
|
18
|
+
* ending_time: 1355759900000, // ms epoch
|
|
19
|
+
* label: 'bar label', // optional
|
|
20
|
+
* color: '#6b0000', // optional, overrides everything
|
|
21
|
+
* },
|
|
22
|
+
* ],
|
|
23
|
+
* },
|
|
24
|
+
* ]
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { select, pointer } from 'd3-selection';
|
|
29
|
+
import { scaleTime, scaleOrdinal } from 'd3-scale';
|
|
30
|
+
import { axisBottom, axisTop } from 'd3-axis';
|
|
31
|
+
import { timeFormat } from 'd3-time-format';
|
|
32
|
+
import { schemeCategory10 } from 'd3-scale-chromatic';
|
|
33
|
+
import { zoom as d3zoom } from 'd3-zoom';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Default rendering options.
|
|
37
|
+
*/
|
|
38
|
+
export const TIMELINE_DEFAULTS = {
|
|
39
|
+
itemHeight: 20,
|
|
40
|
+
itemMargin: 5,
|
|
41
|
+
margin: { top: 30, right: 30, bottom: 30, left: 30 },
|
|
42
|
+
tickSize: 6,
|
|
43
|
+
maxZoomScale: 1024,
|
|
44
|
+
todayFormat: {
|
|
45
|
+
marginTop: 25,
|
|
46
|
+
marginBottom: 0,
|
|
47
|
+
width: 2,
|
|
48
|
+
color: 'rgb(245, 157, 0)',
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Counter used to generate unique clip-path ids inside a document. */
|
|
53
|
+
let clipIdCounter = 0;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Computes the time domain of the chart.
|
|
57
|
+
*
|
|
58
|
+
* Uses `beginning`/`ending` when provided, otherwise scans every time entry
|
|
59
|
+
* for the smallest starting_time and the largest ending_time.
|
|
60
|
+
*
|
|
61
|
+
* @param {Array} data the timeline data array
|
|
62
|
+
* @param {Date|undefined} beginning explicit domain start
|
|
63
|
+
* @param {Date|undefined} ending explicit domain end
|
|
64
|
+
* @return {[Date, Date]|null} the domain, or null if it cannot be computed
|
|
65
|
+
*/
|
|
66
|
+
export function computeDomain(data, beginning, ending) {
|
|
67
|
+
let min = beginning ? beginning.getTime() : Infinity;
|
|
68
|
+
let max = ending ? ending.getTime() : -Infinity;
|
|
69
|
+
|
|
70
|
+
if (!beginning || !ending) {
|
|
71
|
+
for (const series of data) {
|
|
72
|
+
for (const time of series.times || []) {
|
|
73
|
+
if (!beginning && time.starting_time < min) {
|
|
74
|
+
min = time.starting_time;
|
|
75
|
+
}
|
|
76
|
+
if (!ending) {
|
|
77
|
+
const end = time.ending_time !== undefined ? time.ending_time : time.starting_time;
|
|
78
|
+
if (end > max) {
|
|
79
|
+
max = end;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!Number.isFinite(min) || !Number.isFinite(max)) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return [new Date(min), new Date(max)];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Flattens the data array into one bar record per time entry.
|
|
94
|
+
*
|
|
95
|
+
* Row semantics match the original d3-timeline plugin: when `stack` is true
|
|
96
|
+
* each series gets its own row, otherwise everything overlaps on row 0.
|
|
97
|
+
*
|
|
98
|
+
* @param {Array} data the timeline data array
|
|
99
|
+
* @param {boolean} stack whether series are stacked on separate rows
|
|
100
|
+
* @return {Array} flat array of {time, timeIndex, series, seriesIndex, rowIndex}
|
|
101
|
+
*/
|
|
102
|
+
export function flattenData(data, stack) {
|
|
103
|
+
const flat = [];
|
|
104
|
+
data.forEach((series, seriesIndex) => {
|
|
105
|
+
(series.times || []).forEach((time, timeIndex) => {
|
|
106
|
+
flat.push({
|
|
107
|
+
time,
|
|
108
|
+
timeIndex,
|
|
109
|
+
series,
|
|
110
|
+
seriesIndex,
|
|
111
|
+
rowIndex: stack ? seriesIndex : 0,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
return flat;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Resolves the fill color of a bar, in the exact priority order of v2:
|
|
120
|
+
* time.color → colorScale(time[colorsProperty]) → colorScale(series[colorsProperty])
|
|
121
|
+
* → colorScale(seriesIndex).
|
|
122
|
+
*
|
|
123
|
+
* @param {Object} record a flattened bar record
|
|
124
|
+
* @param {Function} colorScale the ordinal color scale
|
|
125
|
+
* @param {string|undefined} colorsProperty the property name mapped to the scale
|
|
126
|
+
* @return {string} a CSS color
|
|
127
|
+
*/
|
|
128
|
+
function resolveColor({ time, series, seriesIndex }, colorScale, colorsProperty) {
|
|
129
|
+
if (time.color) {
|
|
130
|
+
return time.color;
|
|
131
|
+
}
|
|
132
|
+
if (colorsProperty) {
|
|
133
|
+
if (time[colorsProperty] !== undefined) {
|
|
134
|
+
return colorScale(time[colorsProperty]);
|
|
135
|
+
}
|
|
136
|
+
if (series[colorsProperty] !== undefined) {
|
|
137
|
+
return colorScale(series[colorsProperty]);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return colorScale(seriesIndex);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Builds the configured time axis.
|
|
145
|
+
*
|
|
146
|
+
* Tick configuration priority: tickValues → tickTime (+ tickInterval) → numTicks.
|
|
147
|
+
* `tickFormat` accepts either a function or a d3 time-format specifier string.
|
|
148
|
+
*
|
|
149
|
+
* @param {Function} xScale the time scale
|
|
150
|
+
* @param {Object} options the normalized options
|
|
151
|
+
* @return {Function} a configured d3 axis
|
|
152
|
+
*/
|
|
153
|
+
function buildAxis(xScale, options) {
|
|
154
|
+
const axis = (options.axisTop ? axisTop : axisBottom)(xScale);
|
|
155
|
+
|
|
156
|
+
if (options.tickValues) {
|
|
157
|
+
axis.tickValues(options.tickValues.map((v) => (v instanceof Date ? v : new Date(v))));
|
|
158
|
+
} else if (options.tickTime) {
|
|
159
|
+
const interval = options.tickInterval > 1
|
|
160
|
+
? options.tickTime.every(options.tickInterval)
|
|
161
|
+
: options.tickTime;
|
|
162
|
+
if (interval) {
|
|
163
|
+
axis.ticks(interval);
|
|
164
|
+
}
|
|
165
|
+
} else if (options.numTicks) {
|
|
166
|
+
axis.ticks(options.numTicks);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const format = options.tickFormat || '%I %p';
|
|
170
|
+
axis.tickFormat(typeof format === 'function' ? format : timeFormat(format));
|
|
171
|
+
|
|
172
|
+
axis.tickSize(options.tickSize !== undefined ? options.tickSize : TIMELINE_DEFAULTS.tickSize);
|
|
173
|
+
|
|
174
|
+
return axis;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Renders the timeline into `containerEl`, replacing any previous svg.
|
|
179
|
+
*
|
|
180
|
+
* When `options.axisZoom` is set, the time axis can be zoomed with
|
|
181
|
+
* Ctrl/⌘ + mouse wheel (or trackpad pinch), panned by dragging, and zoomed in
|
|
182
|
+
* by double-clicking. `options.onZoom(transform, [start, end])` is called on
|
|
183
|
+
* every user zoom/pan, and a previously saved transform can be restored by
|
|
184
|
+
* passing it back as `options.zoomTransform`.
|
|
185
|
+
*
|
|
186
|
+
* @param {Element} containerEl the element to render into
|
|
187
|
+
* @param {Array} data the timeline data array
|
|
188
|
+
* @param {Object} options normalized options, see granite-timeline.js _buildOptions()
|
|
189
|
+
* @return {{svgNode: SVGElement|null, scale: Function|null}}
|
|
190
|
+
*/
|
|
191
|
+
export function renderTimeline(containerEl, data, options = {}) {
|
|
192
|
+
select(containerEl).selectAll('svg').remove();
|
|
193
|
+
|
|
194
|
+
const itemHeight = options.itemHeight ?? TIMELINE_DEFAULTS.itemHeight;
|
|
195
|
+
const itemMargin = options.itemMargin ?? TIMELINE_DEFAULTS.itemMargin;
|
|
196
|
+
const margin = { ...TIMELINE_DEFAULTS.margin, ...options.margin };
|
|
197
|
+
const width = options.width || 300;
|
|
198
|
+
|
|
199
|
+
const domain = computeDomain(data || [], options.beginning, options.ending);
|
|
200
|
+
if (!domain) {
|
|
201
|
+
return { svgNode: null, scale: null };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const flat = flattenData(data, options.stack);
|
|
205
|
+
const rowCount = options.stack ? data.length : 1;
|
|
206
|
+
const height = options.height
|
|
207
|
+
|| margin.top + rowCount * (itemHeight + itemMargin) + margin.bottom;
|
|
208
|
+
|
|
209
|
+
const rowY = (rowIndex) => margin.top + (itemHeight + itemMargin) * rowIndex;
|
|
210
|
+
|
|
211
|
+
const baseScale = scaleTime()
|
|
212
|
+
.domain(domain)
|
|
213
|
+
.range([margin.left, width - margin.right]);
|
|
214
|
+
|
|
215
|
+
const colorScale = options.colors || scaleOrdinal(schemeCategory10);
|
|
216
|
+
const labelText = (label) => (options.labelFormat ? options.labelFormat(label) : String(label));
|
|
217
|
+
const barEnd = (time) => (time.ending_time !== undefined ? time.ending_time : time.starting_time);
|
|
218
|
+
|
|
219
|
+
const svg = select(containerEl)
|
|
220
|
+
.append('svg')
|
|
221
|
+
.attr('width', width)
|
|
222
|
+
.attr('height', height)
|
|
223
|
+
.attr('class', 'granite-timeline');
|
|
224
|
+
|
|
225
|
+
const svgNode = svg.node();
|
|
226
|
+
|
|
227
|
+
// Row backgrounds
|
|
228
|
+
if (options.background) {
|
|
229
|
+
svg.selectAll('rect.row-background')
|
|
230
|
+
.data(Array.from({ length: rowCount }, (_, i) => i))
|
|
231
|
+
.join('rect')
|
|
232
|
+
.attr('class', 'row-background')
|
|
233
|
+
.attr('x', margin.left)
|
|
234
|
+
.attr('y', (i) => rowY(i) - itemMargin / 2)
|
|
235
|
+
.attr('width', Math.max(0, width - margin.left - margin.right))
|
|
236
|
+
.attr('height', itemHeight + itemMargin)
|
|
237
|
+
.attr('fill', options.background);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Row separators (between rows)
|
|
241
|
+
if (options.rowSeparators && rowCount > 1) {
|
|
242
|
+
svg.selectAll('line.row-separator')
|
|
243
|
+
.data(Array.from({ length: rowCount - 1 }, (_, i) => i + 1))
|
|
244
|
+
.join('line')
|
|
245
|
+
.attr('class', 'row-separator')
|
|
246
|
+
.attr('x1', margin.left)
|
|
247
|
+
.attr('x2', width - margin.right)
|
|
248
|
+
.attr('y1', (i) => rowY(i) - itemMargin / 2)
|
|
249
|
+
.attr('y2', (i) => rowY(i) - itemMargin / 2)
|
|
250
|
+
.attr('stroke', options.rowSeparators)
|
|
251
|
+
.attr('stroke-width', 1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// With zoom enabled, bars / bar labels / today line pan out of the plot
|
|
255
|
+
// area: clip them so they don't bleed under the row labels and margins.
|
|
256
|
+
let plotLayer = svg;
|
|
257
|
+
if (options.axisZoom) {
|
|
258
|
+
const clipId = `granite-timeline-clip-${++clipIdCounter}`;
|
|
259
|
+
svg.append('defs')
|
|
260
|
+
.append('clipPath')
|
|
261
|
+
.attr('id', clipId)
|
|
262
|
+
.append('rect')
|
|
263
|
+
.attr('x', margin.left)
|
|
264
|
+
.attr('y', 0)
|
|
265
|
+
.attr('width', Math.max(0, width - margin.left - margin.right))
|
|
266
|
+
.attr('height', height);
|
|
267
|
+
plotLayer = svg.append('g').attr('clip-path', `url(#${clipId})`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Bars (x-dependent attributes are set in applyScale)
|
|
271
|
+
const bars = plotLayer.selectAll('rect.timeline-bar')
|
|
272
|
+
.data(flat)
|
|
273
|
+
.join('rect')
|
|
274
|
+
.attr('class', (d) => `timeline-bar${d.series.class ? ` ${d.series.class}` : ''}`)
|
|
275
|
+
.attr('y', (d) => rowY(d.rowIndex))
|
|
276
|
+
.attr('height', itemHeight)
|
|
277
|
+
.attr('fill', (d) => resolveColor(d, colorScale, options.colorsProperty));
|
|
278
|
+
|
|
279
|
+
// Per-bar labels
|
|
280
|
+
const barLabels = plotLayer.selectAll('text.timeline-bar-label')
|
|
281
|
+
.data(flat.filter((d) => d.time.label !== undefined))
|
|
282
|
+
.join('text')
|
|
283
|
+
.attr('class', 'timeline-bar-label')
|
|
284
|
+
.attr('y', (d) => rowY(d.rowIndex) + itemHeight * 0.75)
|
|
285
|
+
.text((d) => labelText(d.time.label));
|
|
286
|
+
|
|
287
|
+
// Series row labels (meaningful when stacked)
|
|
288
|
+
if (options.stack) {
|
|
289
|
+
svg.selectAll('text.timeline-label')
|
|
290
|
+
.data(data.map((series, i) => ({ series, i })).filter((d) => d.series.label !== undefined))
|
|
291
|
+
.join('text')
|
|
292
|
+
.attr('class', 'timeline-label')
|
|
293
|
+
.attr('x', Math.max(0, margin.left - 10))
|
|
294
|
+
.attr('y', (d) => rowY(d.i) + itemHeight * 0.75)
|
|
295
|
+
.attr('text-anchor', 'end')
|
|
296
|
+
.text((d) => labelText(d.series.label));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Today line
|
|
300
|
+
let todayLine = null;
|
|
301
|
+
const now = Date.now();
|
|
302
|
+
if (options.showToday && now >= domain[0].getTime() && now <= domain[1].getTime()) {
|
|
303
|
+
const todayFormat = { ...TIMELINE_DEFAULTS.todayFormat, ...options.todayFormat };
|
|
304
|
+
todayLine = plotLayer.append('line')
|
|
305
|
+
.attr('class', 'timeline-today')
|
|
306
|
+
.attr('y1', todayFormat.marginTop)
|
|
307
|
+
.attr('y2', height - todayFormat.marginBottom)
|
|
308
|
+
.attr('stroke', todayFormat.color)
|
|
309
|
+
.attr('stroke-width', todayFormat.width);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Time axis
|
|
313
|
+
let axisG = null;
|
|
314
|
+
if (options.showTimeAxis) {
|
|
315
|
+
axisG = svg.append('g')
|
|
316
|
+
.attr('class', 'axis timeline-axis')
|
|
317
|
+
.attr('transform', `translate(0, ${options.axisTop ? margin.top : height - margin.bottom})`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** (Re)applies a time scale to every x-dependent part of the chart. */
|
|
321
|
+
const applyScale = (scale) => {
|
|
322
|
+
bars
|
|
323
|
+
.attr('x', (d) => scale(d.time.starting_time))
|
|
324
|
+
.attr('width', (d) => Math.max(0, scale(barEnd(d.time)) - scale(d.time.starting_time)));
|
|
325
|
+
barLabels.attr('x', (d) => scale(d.time.starting_time) + 5);
|
|
326
|
+
if (todayLine) {
|
|
327
|
+
todayLine.attr('x1', scale(now)).attr('x2', scale(now));
|
|
328
|
+
}
|
|
329
|
+
if (axisG) {
|
|
330
|
+
axisG.call(buildAxis(scale, options));
|
|
331
|
+
if (options.rotateTicks) {
|
|
332
|
+
const rotate = options.rotateTicks;
|
|
333
|
+
axisG.selectAll('text')
|
|
334
|
+
.attr('transform', `rotate(${rotate})`)
|
|
335
|
+
.attr('dx', rotate < 0 ? '-0.8em' : '0.8em')
|
|
336
|
+
.attr('dy', rotate < 0 ? '0.15em' : '0.55em')
|
|
337
|
+
.style('text-anchor', rotate < 0 ? 'end' : 'start');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
applyScale(baseScale);
|
|
342
|
+
|
|
343
|
+
// Axis zoom & pan
|
|
344
|
+
if (options.axisZoom) {
|
|
345
|
+
const plotExtent = [[margin.left, 0], [width - margin.right, height]];
|
|
346
|
+
const zoomBehavior = d3zoom()
|
|
347
|
+
.scaleExtent([1, options.maxZoomScale ?? TIMELINE_DEFAULTS.maxZoomScale])
|
|
348
|
+
.extent(plotExtent)
|
|
349
|
+
.translateExtent(plotExtent)
|
|
350
|
+
// Plain wheel keeps scrolling the page: zooming needs Ctrl/⌘ (trackpad
|
|
351
|
+
// pinch gestures report ctrlKey and work out of the box).
|
|
352
|
+
.filter((event) => {
|
|
353
|
+
if (event.type === 'wheel') {
|
|
354
|
+
return event.ctrlKey || event.metaKey;
|
|
355
|
+
}
|
|
356
|
+
return !event.button;
|
|
357
|
+
})
|
|
358
|
+
.on('zoom', (event) => {
|
|
359
|
+
const scale = event.transform.rescaleX(baseScale);
|
|
360
|
+
applyScale(scale);
|
|
361
|
+
// Only report user interactions, not programmatic transform restores
|
|
362
|
+
if (options.onZoom && event.sourceEvent) {
|
|
363
|
+
options.onZoom(event.transform, scale.domain());
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
svg.call(zoomBehavior).style('cursor', 'grab');
|
|
367
|
+
if (options.zoomTransform) {
|
|
368
|
+
svg.call(zoomBehavior.transform, options.zoomTransform);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Bar events
|
|
373
|
+
if (options.onBarEvent) {
|
|
374
|
+
const emit = (type) => (event, record) => {
|
|
375
|
+
options.onBarEvent(type, {
|
|
376
|
+
d: record.time,
|
|
377
|
+
index: record.timeIndex,
|
|
378
|
+
datum: record.series,
|
|
379
|
+
mouse: pointer(event, svgNode),
|
|
380
|
+
evt: event,
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
bars
|
|
384
|
+
.on('click', emit('click'))
|
|
385
|
+
.on('mouseover', emit('mouseover'))
|
|
386
|
+
.on('mouseout', emit('mouseout'))
|
|
387
|
+
.on('mousemove', emit('hover'));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return { svgNode, scale: baseScale };
|
|
391
|
+
}
|
package/.eslintrc.json
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": ["eslint:recommended", "google"],
|
|
3
|
-
"parserOptions": {
|
|
4
|
-
"ecmaVersion": 6,
|
|
5
|
-
"sourceType": "module"
|
|
6
|
-
},
|
|
7
|
-
"env": {
|
|
8
|
-
"browser": true
|
|
9
|
-
},
|
|
10
|
-
"plugins": [
|
|
11
|
-
"html"
|
|
12
|
-
],
|
|
13
|
-
"rules": {
|
|
14
|
-
"max-len": [
|
|
15
|
-
"off"
|
|
16
|
-
],
|
|
17
|
-
"block-spacing": "off",
|
|
18
|
-
"brace-style": "off",
|
|
19
|
-
"new-cap": ["error", { "capIsNewExceptions": ["Polymer"] }],
|
|
20
|
-
"no-var": "off",
|
|
21
|
-
"no-console": "off",
|
|
22
|
-
"object-curly-spacing": "off",
|
|
23
|
-
"require-jsdoc": "off"
|
|
24
|
-
},
|
|
25
|
-
"globals": {
|
|
26
|
-
"Polymer": true
|
|
27
|
-
}
|
|
28
|
-
}
|
package/demo/index.html
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes">
|
|
6
|
-
|
|
7
|
-
<title>granite-timeline demo</title>
|
|
8
|
-
|
|
9
|
-
<script src="../../../@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>
|
|
10
|
-
|
|
11
|
-
<script type="module" src="../../../@polymer/iron-demo-helpers/demo-pages-shared-styles.js"></script>
|
|
12
|
-
<script type="module" src="../../../@polymer/iron-demo-helpers/demo-snippet.js"></script>
|
|
13
|
-
<script type="module" src="../granite-timeline.js"></script>
|
|
14
|
-
|
|
15
|
-
<!-- FIXME(polymer-modulizer):
|
|
16
|
-
These imperative modules that innerHTML your HTML are
|
|
17
|
-
a hacky way to be sure that any mixins in included style
|
|
18
|
-
modules are ready before any elements that reference them are
|
|
19
|
-
instantiated, otherwise the CSS @apply mixin polyfill won't be
|
|
20
|
-
able to expand the underlying CSS custom properties.
|
|
21
|
-
See: https://github.com/Polymer/polymer-modulizer/issues/154
|
|
22
|
-
-->
|
|
23
|
-
<script type="module">
|
|
24
|
-
const $_documentContainer = document.createElement('template');
|
|
25
|
-
|
|
26
|
-
$_documentContainer.innerHTML = `<custom-style>
|
|
27
|
-
<style is="custom-style" include="demo-pages-shared-styles">
|
|
28
|
-
</style>
|
|
29
|
-
</custom-style>`;
|
|
30
|
-
|
|
31
|
-
document.body.appendChild($_documentContainer.content);
|
|
32
|
-
</script>
|
|
33
|
-
</head>
|
|
34
|
-
<body>
|
|
35
|
-
<script type="module">
|
|
36
|
-
const $_documentContainer = document.createElement('template');
|
|
37
|
-
|
|
38
|
-
$_documentContainer.innerHTML = `<div class="vertical-section-container centered">
|
|
39
|
-
<h3>Basic granite-timeline demo</h3>
|
|
40
|
-
<demo-snippet>
|
|
41
|
-
<template>
|
|
42
|
-
<granite-timeline data="[{"times":[{"starting_time":1355752800000,"ending_time":1355759900000}, {"starting_time":1355767900000,"ending_time":1355774400000}]},{"times":[{"starting_time":1355759910000,"ending_time":1355761900000}]},{"times":[{"starting_time":1355761910000,"ending_time":1355763910000}]}]" debug=""></granite-timeline>
|
|
43
|
-
</template>
|
|
44
|
-
</demo-snippet>
|
|
45
|
-
</div>`;
|
|
46
|
-
|
|
47
|
-
document.body.appendChild($_documentContainer.content);
|
|
48
|
-
</script>
|
|
49
|
-
</body>
|
|
50
|
-
</html>
|