Repository navigation
Expand file tree
/
Copy pathindex.html
More file actions
3782 lines (3587 loc) · 164 KB
/
Copy pathindex.html
File metadata and controls
3782 lines (3587 loc) · 164 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Open Trends — Free Google Search Trend Analysis tool</title>
<meta name="description" content="Open Trends turns Google Keyword Planner exports into theme-level trend analysis: growth, velocity, seasonality and top searches for any category. Free and open source.">
<meta name="keywords" content="google keyword planner, trend analysis, search trends, keyword trends, google trends alternative, keyword research tool, search volume analysis, theme analysis, seasonality, free trend analysis tool">
<meta name="author" content="Sarth Jauhari">
<meta name="robots" content="index, follow">
<link rel="icon" href="logo-short.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="logo-short.svg">
<link rel="canonical" href="https://opentrends.win/">
<meta name="theme-color" content="#1f2d4a">
<!-- Open Graph -->
<meta property="og:type" content="website">
<meta property="og:site_name" content="Open Trends">
<meta property="og:title" content="Open Trends — Free Google Search Trend Analysis tool">
<meta property="og:description" content="Turn Google Keyword Planner exports into theme-level growth, velocity and seasonality for any category. Free, open source, runs in your browser.">
<meta property="og:url" content="https://opentrends.win/">
<meta property="og:image" content="https://opentrends.win/og-image.png">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Open Trends — Free Google Search Trend Analysis tool">
<meta name="twitter:description" content="Turn Google Keyword Planner exports into theme-level growth, velocity and seasonality for any category. Free, open source, runs in your browser.">
<meta name="twitter:image" content="https://opentrends.win/og-image.png">
<!-- Structured data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Open Trends",
"applicationCategory": "BusinessApplication",
"operatingSystem": "Web browser",
"description": "A free, open-source browser app that turns Google Keyword Planner exports into theme-level trend analysis: growth, velocity, seasonality and top searches for any category.",
"url": "https://opentrends.win/",
"author": { "@type": "Person", "name": "Sarth Jauhari", "url": "https://github.com/saarth" },
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" },
"isAccessibleForFree": true,
"license": "https://github.com/saarth/OpenTrends"
}
</script>
<style>
:root {
--bg: #f6f3ee;
--bg-soft: #efeae1;
--panel: #ffffff;
--ink: #1c1a17;
--ink-soft: #3a3630;
--muted: #7a746a;
--line: #e6e1d6;
--line-strong: #d2ccbf;
--accent: #1f2d4a;
--accent-soft: #e8ebf2;
--gold: #9a7b4f;
--pos: #2f6b4f;
--pos-bg: #eaf3ee;
--neg: #9b3a3a;
--neg-bg: #f6eaea;
--warn: #8a6a2f;
--shadow: 0 1px 0 rgba(20,18,14,0.04), 0 12px 40px rgba(20,18,14,0.06);
--radius: 10px;
--radius-sm: 6px;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--ink);
font-family: "Inter", "Helvetica Neue", "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3, h4 { margin: 0; font-weight: 500; letter-spacing: -0.015em; }
h1 { font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Palatino, Georgia, serif; font-size: 42px; font-weight: 400; line-height: 1.05; letter-spacing: -0.02em; }
h2 { font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Palatino, Georgia, serif; font-size: 24px; font-weight: 400; }
h3 { font-size: 15px; font-weight: 600; letter-spacing: 0.01em; text-transform: none; }
p { margin: 0; }
button, input, select, textarea { font: inherit; }
button { cursor: pointer; }
.hidden { display: none !important; }
.app-header {
position: relative;
overflow: hidden;
color: var(--ink);
padding: 48px 0 40px;
background: var(--bg);
border-bottom: 1px solid var(--line);
}
.app-header::before {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(900px 400px at 85% -10%, rgba(31,45,74,0.05), transparent 60%);
pointer-events: none;
}
.header-inner {
position: relative;
z-index: 1;
max-width: 1520px;
margin: 0 auto;
padding: 0 40px;
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 32px;
}
.brand-mark {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 0;
border: 0;
border-radius: 0;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--muted);
background: transparent;
margin-bottom: 18px;
}
.brand-mark .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--gold); box-shadow: none; }
.brand-logo { height: 120px; width: auto; max-width: 100%; display: block; }
.app-header p { color: var(--muted); max-width: 640px; margin-top: 14px; font-size: 15px; line-height: 1.55; font-weight: 400; }
.header-actions { display: flex; gap: 10px; flex-wrap: wrap; justify-content: flex-end; align-items: center; }
main {
max-width: 1520px;
margin: 0 auto;
padding: 40px 40px 80px;
}
.card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: none;
padding: 28px;
}
.card + .card { margin-top: 24px; }
.card-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
flex-wrap: wrap;
padding-bottom: 14px;
border-bottom: 1px solid var(--line);
}
.card-head h3 { font-size: 13px; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); }
.hint { color: var(--muted); font-size: 12px; font-weight: 400; }
.muted { color: var(--muted); }
.btn {
appearance: none;
border: 1px solid var(--line-strong);
background: transparent;
color: var(--ink);
border-radius: var(--radius-sm);
padding: 9px 16px;
font-weight: 500;
font-size: 13px;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 7px;
white-space: nowrap;
letter-spacing: 0.01em;
}
.btn:hover { border-color: var(--ink); background: var(--ink); color: #fff; }
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; box-shadow: none; }
.btn.primary:hover { background: #152238; border-color: #152238; filter: none; }
.btn.ghost-light { background: transparent; border-color: var(--line-strong); color: var(--ink); }
.btn.ghost-light:hover { background: var(--ink); color: #fff; border-color: var(--ink); }
.btn:disabled { opacity: 0.35; cursor: not-allowed; }
.btn:disabled:hover { background: transparent; color: var(--ink); border-color: var(--line-strong); }
.btn.small { padding: 6px 12px; font-size: 12px; border-radius: var(--radius-sm); }
.setup-grid {
display: grid;
grid-template-columns: minmax(0, 1.5fr) minmax(320px, 0.7fr);
gap: 32px;
align-items: start;
}
.drop-zone {
position: relative;
border: 1px dashed var(--line-strong);
border-radius: var(--radius);
background: var(--panel);
padding: 48px 32px;
text-align: center;
transition: 0.2s ease;
min-height: 260px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
}
.drop-zone.dragover { border-color: var(--accent); background: var(--accent-soft); }
.drop-icon {
width: 48px; height: 48px; border-radius: 50%;
background: var(--bg-soft);
display: grid; place-items: center;
font-size: 20px;
color: var(--accent);
}
.drop-zone h2 { font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif; font-size: 22px; font-weight: 400; }
.drop-zone p { color: var(--muted); font-size: 13px; max-width: 460px; line-height: 1.5; }
.file-info {
margin-top: 16px;
border: 1px solid var(--line);
background: var(--panel);
border-radius: var(--radius);
padding: 14px 16px;
font-size: 13px;
}
.file-info strong { font-weight: 600; }
.file-info .meta { color: var(--muted); margin-top: 4px; font-size: 12px; }
.settings { display: flex; flex-direction: column; gap: 16px; }
label { display: flex; flex-direction: column; gap: 6px; font-size: 12px; font-weight: 500; color: var(--muted); letter-spacing: 0.02em; }
input[type="text"], input[type="number"], input[type="search"], select, textarea {
width: 100%;
border: 1px solid var(--line-strong);
border-radius: var(--radius-sm);
background: var(--panel);
color: var(--ink);
padding: 10px 12px;
outline: none;
transition: 0.2s ease;
font-weight: 400;
}
input:focus, select:focus, textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(31,45,74,0.08); }
.row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.row > label { flex: 1 1 150px; }
.check { flex-direction: row; align-items: center; gap: 9px; font-weight: 400; color: var(--ink-soft); font-size: 13px; }
.check input { width: auto; accent-color: var(--accent); }
.status { min-height: 18px; font-size: 12px; color: var(--muted); }
.status.error { color: var(--neg); }
.status.ok { color: var(--pos); }
details.dict { margin-top: 20px; border-top: 1px solid var(--line); padding-top: 18px; }
details.dict summary { cursor: pointer; font-weight: 500; font-size: 13px; color: var(--ink); display: flex; gap: 8px; align-items: center; letter-spacing: 0.02em; }
details.dict summary::marker { color: var(--accent); }
details.dict p { color: var(--muted); font-size: 12.5px; margin: 12px 0; line-height: 1.6; }
details.dict textarea {
min-height: 220px;
font-family: "SF Mono", "JetBrains Mono", ui-monospace, SFMono-Regular, Consolas, monospace;
font-size: 11.5px;
line-height: 1.55;
background: #1a1a1a;
color: #e8e4dc;
border-color: #1a1a1a;
border-radius: var(--radius-sm);
resize: vertical;
}
details.dict .row { margin-top: 12px; }
.kpi-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.kpi {
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 20px 22px;
box-shadow: none;
position: relative;
overflow: hidden;
}
.kpi::before { content: none; }
.kpi .label { color: var(--muted); font-size: 10.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.14em; }
.kpi .value { font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif; font-size: 30px; font-weight: 400; letter-spacing: -0.02em; margin-top: 10px; color: var(--ink); }
.kpi .sub { color: var(--muted); font-size: 12px; margin-top: 6px; line-height: 1.4; }
.tabs {
display: flex;
gap: 2px;
padding: 4px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--panel);
margin-bottom: 24px;
overflow-x: auto;
position: sticky;
top: 12px;
z-index: 20;
}
.tab {
border: 0;
background: transparent;
color: var(--muted);
padding: 10px 16px;
border-radius: var(--radius-sm);
font-weight: 500;
font-size: 13px;
white-space: nowrap;
letter-spacing: 0.01em;
transition: 0.2s ease;
}
.tab:hover { color: var(--ink); }
.tab.active { background: var(--accent); color: #fff; }
.grid-2 { display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr); gap: 24px; }
.grid-2 > .card { margin-top: 0; }
.chart-wrap { position: relative; height: 360px; }
.chart-wrap.short { height: 280px; }
canvas { max-width: 100%; }
.table-wrap { overflow: auto; border: 1px solid var(--line); border-radius: var(--radius); max-height: 620px; background: var(--panel); }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid var(--line); vertical-align: middle; }
th {
position: sticky;
top: 0;
z-index: 2;
background: var(--panel);
color: var(--muted);
font-size: 10.5px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.12em;
white-space: nowrap;
cursor: pointer;
user-select: none;
border-bottom: 1px solid var(--line-strong);
}
th:hover { color: var(--ink); }
th .sort-arrow { opacity: 0.4; margin-left: 4px; font-size: 9px; }
tbody tr { transition: background 0.15s ease; }
tbody tr:hover td { background: var(--bg-soft); }
tbody tr.selected td { background: var(--accent-soft); }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
td.nowrap { white-space: nowrap; }
.family-name { font-weight: 500; color: var(--ink); }
.rank { display: inline-grid; place-items: center; width: 22px; height: 22px; border-radius: 4px; background: var(--bg-soft); color: var(--muted); font-size: 11px; font-weight: 600; }
.growth { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: 600; white-space: nowrap; font-variant-numeric: tabular-nums; }
.growth.pos { color: var(--pos); background: var(--pos-bg); }
.growth.neg { color: var(--neg); background: var(--neg-bg); }
.growth.na { color: var(--muted); background: var(--bg-soft); font-weight: 500; }
.bar-track { height: 4px; border-radius: 2px; background: var(--bg-soft); overflow: hidden; min-width: 60px; }
.bar-fill { height: 100%; border-radius: 2px; background: var(--accent); }
.chips { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }
.chip {
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid var(--line);
background: var(--panel);
border-radius: 999px;
padding: 6px 12px;
font-size: 12px;
font-weight: 500;
color: var(--ink-soft);
cursor: pointer;
user-select: none;
transition: 0.2s ease;
}
.chip:hover { border-color: var(--line-strong); }
.chip input { accent-color: var(--accent); width: auto; }
.chip.active { border-color: var(--accent); background: var(--accent); color: #fff; }
.empty { padding: 40px 24px; text-align: center; color: var(--muted); font-size: 13px; font-style: italic; }
.note {
background: var(--panel);
border: 1px solid var(--line);
border-left: 2px solid var(--accent);
border-radius: var(--radius);
padding: 14px 18px;
color: var(--ink-soft);
font-size: 13px;
line-height: 1.6;
}
.note b { color: var(--ink); font-weight: 600; }
.legend { display: flex; gap: 16px; flex-wrap: wrap; color: var(--muted); font-size: 12px; margin-top: 10px; }
.legend .swatch { width: 10px; height: 10px; border-radius: 2px; display: inline-block; margin-right: 6px; vertical-align: middle; }
.heat { font-variant-numeric: tabular-nums; font-weight: 600; }
.heat.low { color: var(--muted); }
.heat.high { color: var(--accent); }
.driver-list { display: flex; flex-direction: column; gap: 10px; }
.driver-item {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
padding: 14px 16px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--panel);
}
.driver-item .driver-label {
color: var(--muted);
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.12em;
margin-bottom: 4px;
}
.driver-item .driver-value { font-weight: 500; color: var(--ink); font-size: 14px; }
.driver-item .driver-detail { color: var(--muted); font-size: 12px; text-align: right; white-space: nowrap; }
.driver-item .driver-detail strong { color: var(--ink); font-weight: 600; }
.delta-pos { color: var(--pos); font-weight: 600; font-variant-numeric: tabular-nums; }
.delta-neg { color: var(--neg); font-weight: 600; font-variant-numeric: tabular-nums; }
.delta-na { color: var(--muted); font-weight: 500; }
#toast {
position: fixed;
right: 24px;
bottom: 24px;
background: var(--ink);
color: #fff;
padding: 12px 18px;
border-radius: var(--radius-sm);
box-shadow: 0 12px 32px rgba(0,0,0,0.18);
font-size: 13px;
opacity: 0;
transform: translateY(8px);
transition: 0.25s ease;
pointer-events: none;
z-index: 100;
max-width: 380px;
letter-spacing: 0.01em;
}
#toast.show { opacity: 1; transform: translateY(0); }
#toast.error { background: var(--neg); }
.footer-note { color: var(--muted); font-size: 12px; margin-top: 24px; text-align: center; letter-spacing: 0.02em; }
.footer-note a { color: var(--muted); }
.footer-note a:hover { color: var(--ink); }
@media (max-width: 1100px) {
.kpi-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-2 { grid-template-columns: 1fr; }
.setup-grid { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
.header-inner { flex-direction: column; align-items: flex-start; gap: 16px; }
h1 { font-size: 30px; }
.brand-logo { height: 72px; }
main { padding: 24px 20px 60px; }
.card { padding: 20px; }
.kpi-grid { grid-template-columns: 1fr; }
.chart-wrap { height: 300px; }
.header-actions { width: 100%; }
}
</style>
</head>
<body>
<header class="app-header">
<div class="header-inner">
<div>
<div class="brand-mark"><img src="logo-short.svg" alt="" width="14" height="14" style="display:block"> Trend intelligence</div>
<h1 style="margin:0"><a href="index.html" style="display:inline-block"><img src="logo-full.svg" alt="OpenTrends" class="brand-logo"></a></h1>
<p>Turn Google Keyword Planner exports into theme-level search trend analysis for any category: growth, velocity, seasonality and top searches. You supply the category searches and the keyword buckets; Open Trends reports what is moving. Free, open source and runs entirely in your browser.</p>
</div>
<div class="header-actions">
<a class="btn ghost-light" href="about.html">About</a>
<a class="btn ghost-light" href="faq.html">FAQ</a>
<button class="btn ghost-light" id="headerDefaultBtn">Reset to example definitions</button>
<button class="btn ghost-light" id="headerExportBtn" disabled>Export Excel</button>
</div>
</div>
</header>
<main>
<section class="card" id="setupCard">
<div class="setup-grid">
<div>
<div class="drop-zone" id="dropZone">
<div class="drop-icon">^</div>
<h2>Drop a Google Keyword Planner export</h2>
<p>XLSX, XLS or CSV. The tool looks for a keyword column, an AMS / average monthly searches column, and monthly date columns.</p>
<button class="btn primary" id="browseBtn" type="button">Choose file</button>
<input type="file" id="fileInput" accept=".xlsx,.xls,.csv,text/csv" class="hidden">
</div>
<div class="file-info hidden" id="fileInfo"></div>
</div>
<div>
<div class="card" style="margin-top:16px">
<div class="card-head"><h3>Category definitions</h3></div>
<p class="hint" style="margin-bottom:12px">Upload your keyword buckets as a dictionary (.json, .xlsx, .xls or .csv): one row per theme, with a boolean query or a list of terms. The example file shows the expected columns (Theme, Query).</p>
<div class="row">
<button class="btn small" id="uploadCategoryBtn" type="button">Upload definitions</button>
<a class="btn small ghost-light" href="example-categories.xlsx" download>Example definitions (.xlsx)</a>
<a class="btn small ghost-light" href="example-categories.json" download>Example definitions (.json)</a>
<input type="file" id="categoryFile" accept=".json,.xlsx,.xls,.csv" class="hidden">
</div>
</div>
<div class="card" style="margin-top:16px">
<div class="card-head"><h3>Brand & product dictionary</h3></div>
<p class="hint" style="margin-bottom:12px">Upload brand and product-type dictionaries (.json, .xlsx, .xls or .csv). Use "Upload combined" for a single file with both, or upload them separately.</p>
<div class="row">
<button class="btn small" id="uploadBrandsBtn2" type="button">Upload brands</button>
<button class="btn small" id="uploadTypesBtn2" type="button">Upload product types</button>
<button class="btn small" id="uploadBrandDictBtn2" type="button">Upload combined</button>
<input type="file" id="brandFile2" accept=".json,.xlsx,.xls,.csv" class="hidden">
<input type="file" id="typeFile2" accept=".json,.xlsx,.xls,.csv" class="hidden">
<input type="file" id="brandDictFile2" accept=".json,.xlsx,.xls,.csv" class="hidden">
</div>
</div>
<div class="card" style="margin-top:16px">
<div class="card-head"><h3>Example data</h3></div>
<p class="hint" style="margin-bottom:12px">A small Google Keyword Planner export with the expected columns (keyword, AMS, 48 monthly columns, Oct 2022 - Sep 2026) to try the tool.</p>
<div class="row">
<a class="btn small ghost-light" href="example-gkp.xlsx" download>Example GKP export (.xlsx)</a>
</div>
</div>
<div class="settings">
<label>Worksheet
<select id="sheetSelect" disabled><option>Load a file first</option></select>
</label>
<div class="row">
<label>Short window (months)
<input type="number" id="shortWindow" value="12" min="2" max="36" step="1">
</label>
<label>Long window (months)
<input type="number" id="longWindow" value="48" min="6" max="120" step="1">
</label>
</div>
<label class="check"><input type="checkbox" id="dedupe" checked> Merge duplicate keywords (keep first)</label>
<button class="btn primary" id="runBtn" disabled>Run analysis</button>
<div class="status" id="parseStatus"></div>
</div>
</div>
</div>
<details class="dict" id="dictDetails">
<summary>Category definitions <span class="hint" id="dictCount"></span></summary>
<p>Each key is a theme (your keyword bucket); each value is either a <b>boolean query</b> or a plain list of terms. Queries support <b>OR</b>, <b>AND</b>, <b>NOT</b>, <b>NEAR/3</b>, parentheses, <b>"quoted phrases"</b>, <b>*</b>/<b>?</b> wildcards and <b>#hashtags</b>, and are matched against the words of each search term - so <code>(running OR jogging) NEAR/3 (shoe OR sneakers) NOT (recipe)</code> behaves as written. Term lists keep the older forgiving substring matching (whole words for terms of 4 characters or fewer).</p>
<textarea id="dictText" spellcheck="false"></textarea>
<div class="row">
<button class="btn small" id="loadDefaultBtn" type="button">Load example definitions</button>
<button class="btn small" id="loadWorkbookBtn" type="button">Load workbook term list</button>
<button class="btn small" id="applyDictBtn" type="button">Apply dictionary</button>
<button class="btn small" id="uploadDictBtn" type="button">Upload dictionary</button>
<button class="btn small" id="downloadDictBtn" type="button">Download JSON</button>
<input type="file" id="dictFile" accept=".json,.xlsx,.xls,.csv" class="hidden">
</div>
<label class="check" style="margin-top:10px"><input type="checkbox" id="contextToggle" checked> Treat product words as search context (shoes, sneakers, boots, sandals, trainers, heels, flats)</label>
<label class="check" style="margin-top:6px"><input type="checkbox" id="bareToggle" checked> Also match bare descriptor searches ("running", "basketball", "hiking", "casual")</label>
<p class="hint" style="margin-top:6px">These options keep short searches inside a theme. The first also accepts a product word, so a search like "running shoe" counts even when the definition only lists descriptor words. The second also accepts a search that is <i>only</i> descriptors and filler words, so "running" counts too - while unrelated searches stay out. They only matter for queries written as <i>descriptor near context</i>; plain term lists are unaffected.</p>
</details>
<details class="dict" id="brandDictDetails">
<summary>Brand & product dictionary <span class="hint" id="brandDictCount"></span></summary>
<p>Brands and formats are optional. Each key is a brand or a product type, and the value is a list of aliases matched against keywords the same way as terms. The bundled lists are a sneakers & footwear starter set; replace them with your own. Signature product names are included as aliases, so "air force 1" counts as Nike. Product types classify the format being searched for (sneakers, boots, sandals, heels).</p>
<label style="margin-top:10px">Brands
<textarea id="brandText" spellcheck="false" style="min-height:180px"></textarea>
</label>
<label style="margin-top:10px">Product types
<textarea id="typeText" spellcheck="false" style="min-height:150px"></textarea>
</label>
<div class="row">
<button class="btn small" id="loadDefaultBrandsBtn" type="button">Load example brand list</button>
<button class="btn small" id="uploadBrandsBtn" type="button">Upload brands</button>
<button class="btn small" id="uploadTypesBtn" type="button">Upload product types</button>
<button class="btn small" id="uploadBrandDictBtn" type="button">Upload combined</button>
<button class="btn small" id="applyBrandsBtn" type="button">Apply dictionaries</button>
</div>
<input type="file" id="brandFile" accept=".json,.xlsx,.xls,.csv" class="hidden">
<input type="file" id="typeFile" accept=".json,.xlsx,.xls,.csv" class="hidden">
<input type="file" id="brandDictFile" accept=".json,.xlsx,.xls,.csv" class="hidden">
<label class="check" style="margin-top:10px"><input type="checkbox" id="fuzzyToggle"> Fuzzy match brand aliases (catch novel typos like "barakat" -> "baccarat"; off by default)</label>
<p class="hint" style="margin-top:6px">When on, an unmatched search is attributed to a brand if one of its aliases is within one edit of a search word, and only when exactly one brand matches. Use it for messy exports; leave it off for clean data to avoid false positives.</p>
</details>
</section>
<section id="results" class="hidden">
<div class="kpi-grid" id="kpiGrid"></div>
<nav class="tabs" id="tabs">
<button class="tab active" data-tab="overview">Overview</button>
<button class="tab" data-tab="family">Theme growth</button>
<button class="tab" data-tab="deep">Theme deep dive</button>
<button class="tab" data-tab="terms">Terms</button>
<button class="tab" data-tab="brands">Brands & products</button>
<button class="tab" data-tab="top">Top searches</button>
<button class="tab" data-tab="season">Seasonality</button>
<button class="tab" data-tab="trend">Trend line</button>
<button class="tab" data-tab="export">Export</button>
</nav>
<section class="tab-panel" id="tab-overview">
<div class="grid-2">
<div class="card">
<div class="card-head">
<h3>Velocity map</h3>
<span class="hint">Bubble size = AMS | x = long-term growth | y = short-term growth</span>
</div>
<div class="chart-wrap"><canvas id="bubbleChart"></canvas></div>
</div>
<div class="card">
<div class="card-head">
<h3>Fastest growing themes</h3>
<span class="hint">Ranked by short-term growth</span>
</div>
<div class="table-wrap" id="fastestTable" style="max-height:350px"></div>
</div>
</div>
<div class="card" id="coverageCard" style="margin-top:18px">
<div class="card-head">
<h3>Coverage</h3>
<button class="btn small" id="exportUnmatchedBtn" type="button">Download unclassified CSV</button>
</div>
<div id="coverageBody"></div>
</div>
<div class="card" style="margin-top:18px">
<div class="card-head"><h3>Reading the output</h3></div>
<div class="note">
<b>Size</b> is the sum of average monthly searches (AMS) for every keyword tagged with a theme - it is a scale signal, not a monthly volume.
<b>Short-term growth</b> compares the latest month with the start of the short window (default 12 months).
<b>Long-term growth</b> does the same over the long window (default 48 months).
A theme with high growth but small size is an emerging niche; high growth plus large size is a genuine momentum shift.
Themes and terms can overlap: one keyword can match several terms or themes, so shares can sum above 100% and term-level volumes are not mutually exclusive.
</div>
</div>
</section>
<section class="tab-panel hidden" id="tab-family">
<div class="card">
<div class="card-head">
<h3>Theme growth</h3>
<span class="hint">Click a row to chart that theme</span>
</div>
<div class="table-wrap" id="familyTable"></div>
</div>
<div class="card">
<div class="card-head">
<h3 id="familyChartTitle">Monthly volume</h3>
<label style="flex-direction:row;align-items:center;gap:8px;font-weight:500;color:var(--muted)">
Theme
<select id="familySelect" style="width:auto;min-width:180px"></select>
</label>
</div>
<div class="chart-wrap short"><canvas id="familyChart"></canvas></div>
</div>
</section>
<section class="tab-panel hidden" id="tab-deep">
<div class="card">
<div class="card-head">
<h3>Theme deep dive</h3>
<span class="hint">Which terms are driving this theme's trend?</span>
</div>
<div class="row">
<label style="flex:0 0 240px">Theme <select id="deepFamily"></select></label>
<label style="flex:0 0 220px">Window
<select id="deepWindow">
<option value="short">Short-term (12 months)</option>
<option value="long">Long-term (48 months)</option>
</select>
</label>
</div>
</div>
<div class="kpi-grid" id="deepKpis" style="margin-top:18px"></div>
<div class="grid-2">
<div class="card">
<div class="card-head">
<h3 id="driverChartTitle">Term contribution</h3>
<span class="hint">Change in monthly volume vs window start</span>
</div>
<div class="chart-wrap short"><canvas id="driverChart"></canvas></div>
</div>
<div class="card">
<div class="card-head"><h3>Driver summary</h3></div>
<div id="driverSummary"></div>
</div>
</div>
<div class="card">
<div class="card-head">
<h3>Term-level drivers</h3>
<span class="hint">Sorted by volume change - contributions overlap when a keyword matches several terms</span>
</div>
<div class="table-wrap" id="driverTermTable"></div>
</div>
<div class="card">
<div class="card-head">
<h3>Keyword-level drivers</h3>
<span class="hint">Top risers and decliners behind the change</span>
</div>
<div class="table-wrap" id="driverKeywordTable"></div>
</div>
<div class="grid-2">
<div class="card">
<div class="card-head">
<h3>Brands in this theme</h3>
<span class="hint">Share of the theme's AMS</span>
</div>
<div class="table-wrap" id="deepBrandTable"></div>
</div>
<div class="card">
<div class="card-head">
<h3>Product types in this theme</h3>
<span class="hint">e.g. sneakers, boots, sandals, heels.</span>
</div>
<div class="table-wrap" id="deepTypeTable"></div>
</div>
</div>
</section>
<section class="tab-panel hidden" id="tab-terms">
<div class="card">
<div class="card-head">
<h3>Term growth</h3>
<span class="hint">Within-theme term performance</span>
</div>
<div class="row" style="margin-bottom:12px">
<label style="flex:0 0 190px">Theme <select id="termFamily"></select></label>
<label style="flex:1 1 180px">Search term <input type="search" id="termSearch" placeholder="e.g. vanilla"></label>
<label style="flex:0 0 180px">Chart metric
<select id="termMetric">
<option value="size">Size (AMS)</option>
<option value="short">Short-term growth</option>
<option value="long">Long-term growth</option>
</select>
</label>
</div>
<div class="chart-wrap short"><canvas id="termChart"></canvas></div>
<div class="table-wrap" id="termTable" style="margin-top:14px"></div>
<div class="card" style="margin-top:18px">
<div class="card-head">
<h3 id="termDrillTitle">Keywords in this term</h3>
<span class="hint">Click a term row above - sorted by AMS</span>
<button class="btn small" id="exportTermDrillCsvBtn" type="button">Download CSV</button>
</div>
<div class="table-wrap" id="termDrillTable"></div>
</div>
</div>
</section>
<section class="tab-panel hidden" id="tab-brands">
<div class="card">
<div class="card-head">
<h3>Brands & products</h3>
<span class="hint">Who and what is behind the searches</span>
</div>
<div class="row">
<label style="flex:0 0 170px">View
<select id="brandDim">
<option value="brand">Brands</option>
<option value="type">Product types</option>
</select>
</label>
<label style="flex:0 0 240px">Keyword term
<select id="brandTerm"></select>
</label>
<label style="flex:0 0 170px">Rank by
<select id="brandMetric">
<option value="size">Size (AMS)</option>
<option value="short">Short-term growth</option>
<option value="long">Long-term growth</option>
</select>
</label>
<label style="flex:1 1 180px">Search
<input type="search" id="brandSearch" placeholder="e.g. nike, running">
</label>
</div>
<div class="chart-wrap short" style="margin-top:14px"><canvas id="brandChart"></canvas></div>
<div class="table-wrap" id="brandTable" style="margin-top:14px"></div>
<p class="hint" style="margin-top:10px">Brand totals overlap when a keyword mentions more than one brand, and product types overlap by design (a "car air freshener" is both a car freshener and an air freshener).</p>
</div>
<div class="card">
<div class="card-head">
<h3 id="brandSpotlightTitle">Brand spotlight</h3>
<span class="hint">Click any row to inspect its top products</span>
</div>
<div class="table-wrap" id="brandProducts"></div>
</div>
</section>
<section class="tab-panel hidden" id="tab-top">
<div class="card">
<div class="card-head">
<h3>Top searches by theme</h3>
<span class="hint">Top keywords by AMS</span>
</div>
<div class="row" style="margin-bottom:12px">
<label style="flex:0 0 210px">Theme <select id="topFamily"></select></label>
<label style="flex:0 0 230px">Keyword term <select id="topTerm"></select></label>
<label style="flex:1 1 200px">Search keyword <input type="search" id="topSearch" placeholder="Filter keywords"></label>
<span class="hint" id="topCount"></span>
</div>
<div class="table-wrap" id="topTable"></div>
</div>
</section>
<section class="tab-panel hidden" id="tab-season">
<div class="card">
<div class="card-head">
<h3>Seasonality</h3>
<div class="row" style="gap:8px">
<label style="flex-direction:row;align-items:center;gap:8px;font-weight:500;color:var(--muted)">
Keyword term
<select id="seasonTerm" style="width:auto;min-width:210px"></select>
</label>
<label style="flex-direction:row;align-items:center;gap:8px;font-weight:500;color:var(--muted)">
View
<select id="seasonView" style="width:auto">
<option value="raw">Monthly volume</option>
<option value="index">Seasonality index</option>
</select>
</label>
</div>
</div>
<div class="chips" id="seasonToggles"></div>
<div class="chart-wrap"><canvas id="seasonChart"></canvas></div>
<div class="legend" id="seasonLegend"></div>
</div>
<div class="card">
<div class="card-head">
<h3>Seasonality index by month</h3>
<span class="hint" id="seasonTableHint">100 = an average month for that theme</span>
</div>
<div class="table-wrap" id="seasonTable"></div>
</div>
</section>
<section class="tab-panel hidden" id="tab-trend">
<div class="card">
<div class="card-head">
<h3>Trend line</h3>
<span class="hint">Chart monthly search volumes over time</span>
</div>
<div class="row" style="margin-bottom:12px">
<label style="flex:0 0 280px">Series
<select id="trendSeries"></select>
</label>
<label style="flex:0 0 180px">Time period
<select id="trendPeriod">
<option value="all">All data</option>
<option value="12">Last 12 months</option>
<option value="24">Last 24 months</option>
<option value="48">Last 48 months</option>
</select>
</label>
</div>
<div class="chart-wrap"><canvas id="trendChart"></canvas></div>
<div class="legend" id="trendLegend"></div>
</div>
</section>
<section class="tab-panel hidden" id="tab-export">
<div class="card">
<div class="card-head"><h3>Export</h3></div>
<p class="muted" style="margin-bottom:14px">Download a workbook shaped like the original analysis, or take individual tables into Excel / BI tools.</p>
<div class="row">
<button class="btn primary" id="exportExcelBtn">Download full Excel workbook</button>
<button class="btn" id="exportFamilyCsvBtn">Theme CSV</button>
<button class="btn" id="exportTermCsvBtn">Term CSV</button>
<button class="btn" id="exportDriverTermCsvBtn">Term drivers CSV</button>
<button class="btn" id="exportDriverKeywordCsvBtn">Keyword drivers CSV</button>
<button class="btn" id="exportBrandCsvBtn">Brands CSV</button>
<button class="btn" id="exportTypeCsvBtn">Product types CSV</button>
<button class="btn" id="exportTopCsvBtn">Top searches CSV</button>
<button class="btn" id="exportJsonBtn">Full JSON</button>
</div>
<div class="note" style="margin-top:16px">
<b>Workbook sheets:</b> Summary | Theme Growth | Term Growth | Term Drivers | Keyword Drivers | Brand Growth | Product Types | Brand Mix | Top Searches | Seasonality Index | Monthly Volumes | Classification. Growth values are written as percentages.
</div>
</div>
<div class="card">
<div class="card-head"><h3>Method</h3></div>
<div class="note">
Rows are tagged by testing each dictionary query or term against the keyword. A row joins a theme if any of that theme's terms match.
Theme monthly volume is the sum of the row's monthly volumes; theme size is the sum of AMS. Growth is
<b>(latest - window start) / window start</b>, using the last month in the data and the requested window length.
This mirrors the "1Y Growth" and "4Y Growth%" columns in the manual workbook.
</div>
</div>
</section>
</section>
<p class="footer-note">Free and open source. No ads, no tracking. Runs entirely in your browser; your data is not uploaded anywhere. Self-hostable via GitHub. · Built by Sarth Jauhari · <a href="about.html">About</a> · <a href="faq.html">FAQ</a></p>
<div style="text-align:center;margin-top:16px;display:flex;gap:10px;justify-content:center;flex-wrap:wrap">
<a class="btn ghost-light small" href="https://github.com/saarth/OpenTrends" target="_blank" rel="noopener" aria-label="GitHub" title="GitHub"><svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg></a>
<a class="btn ghost-light small" href="https://www.linkedin.com/in/saarth" target="_blank" rel="noopener" aria-label="LinkedIn" title="LinkedIn"><svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.063 2.063 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg></a>
</div>
</main>
<div id="toast"></div>
<script src="vendor/xlsx.full.min.js"></script>
<script src="vendor/chart.umd.min.js"></script>
<script src="default_classification.js?v=8"></script>
<script src="default_groups.js?v=2"></script>
<script src="default_brands.js?v=4"></script>
<script src="analysis.js?v=13"></script>
<script src="sneakers-trends.js?v=1"></script>
<script>
(function () {
'use strict';
var $ = function (id) { return document.getElementById(id); };
var state = {
workbook: null,
sheetNames: [],
parsed: null,
result: null,
classification: null,
brandDict: null,
typeDict: null,
brandResult: null,
typeResult: null,
crossBrands: null,
crossTypes: null,
coverage: null,
selectedBrand: null,
activeTab: 'overview',
selectedFamily: null,
selectedTerm: null,
charts: {},
sort: {
family: { key: 'size', dir: 'desc' },
term: { key: 'size', dir: 'desc' },
top: { key: 'ams', dir: 'desc' },
deepTerm: { key: 'change', dir: 'desc' },
brand: { key: 'size', dir: 'desc' },
termDrill: { key: 'ams', dir: 'desc' }
},
seasonFamilies: []
};
var COLORS = ['#5b4bff', '#00a896', '#ff7a59', '#e5484d', '#2e90fa', '#9b51e0', '#f2c94c', '#219653', '#eb5757', '#56ccf2', '#bb6bd9', '#7a5cff'];
// Product words that count as "search context" next to a note, so a keyword
// like "running shoe" still lands in a theme.
var PRODUCT_CONTEXT = [
'shoe', 'shoes', 'sneaker', 'sneakers', 'trainer', 'trainers', 'footwear',
'boot', 'boots', 'sandal', 'sandals', 'slipper', 'slippers', 'heel', 'heels',
'flat', 'flats', 'loafer', 'loafers', 'clog', 'clogs', 'mule', 'mules',
'wedge', 'wedges', 'pump', 'pumps', 'cleat', 'cleats', 'court', 'kicks',
'runner', 'runners'
];
function showToast(message, isError) {
var toast = $('toast');
toast.textContent = message;
toast.className = 'show' + (isError ? ' error' : '');
clearTimeout(showToast._timer);
showToast._timer = setTimeout(function () { toast.className = ''; }, 3200);
}
function esc(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
function fmtInt(value) {
if (value == null || !Number.isFinite(Number(value))) return ' - ';
return new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(Number(value));
}
function fmtCompact(value) {
if (value == null || !Number.isFinite(Number(value))) return ' - ';
return new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 }).format(Number(value));
}
function fmtPct(value) {
if (value == null || !Number.isFinite(Number(value))) return ' - ';
return (Number(value) * 100).toFixed(1) + '%';
}
function growthHtml(value) {
if (value == null || !Number.isFinite(Number(value))) return '<span class="growth na"> - </span>';
var positive = Number(value) >= 0;
return '<span class="growth ' + (positive ? 'pos' : 'neg') + '">' +
(positive ? '+' : '-') + ' ' + fmtPct(Math.abs(Number(value))) + '</span>';
}
function fmtSignedInt(value) {
if (value == null || !Number.isFinite(Number(value))) return ' - ';
var number = Number(value);
return (number > 0 ? '+' : '') + fmtInt(number);
}
function fmtPp(value) {
if (value == null || !Number.isFinite(Number(value))) return ' - ';
var number = Number(value) * 100;
return (number > 0 ? '+' : '') + number.toFixed(1) + ' pp';
}
function deltaHtml(value) {
if (value == null || !Number.isFinite(Number(value))) return '<span class="delta-na"> - </span>';
var number = Number(value);
return '<span class="' + (number >= 0 ? 'delta-pos' : 'delta-neg') + '">' + fmtSignedInt(number) + '</span>';
}
function fmtMonth(iso) {
if (!iso) return ' - ';