This commit is contained in:
MuslemRahimi 2025-03-03 13:30:56 +01:00
parent eb06476f38
commit a4a11b2638
8 changed files with 25 additions and 539 deletions

29
package-lock.json generated
View File

@ -66,21 +66,16 @@
"svelte-check": "^3.6.9",
"svelte-echarts": "^1.0.0-rc3",
"svelte-french-toast": "^1.2.0",
"svelte-intersection-observer": "^1.0.0",
"svelte-intersection-observer-action": "^0.0.5",
"svelte-inview": "^4.0.2",
"svelte-lazy": "^1.2.11",
"svelte-lightweight-charts": "^2.2.0",
"svelte-loading-spinners": "^0.3.6",
"svelte-preprocess": "^5.1.4",
"svelte-progress-bar": "^3.0.2",
"svelte-sonner": "^0.3.27",
"svelte-tags-input": "^6.0.1",
"svelte-tiny-virtual-list": "^2.1.2",
"tailwind-merge": "^2.5.2",
"tailwind-variants": "^0.2.1",
"tailwindcss": "^4.0.9",
"tslib": "^2.7.0",
"typescript": "^5.4.5",
"util": "^0.12.5",
"uuid": "^10.0.0",
@ -8692,18 +8687,6 @@
"svelte": "^3.19.0 || ^4.0.0"
}
},
"node_modules/svelte-intersection-observer": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/svelte-intersection-observer/-/svelte-intersection-observer-1.0.0.tgz",
"integrity": "sha512-AoxSog8fSt9sSN8ajMYM1I48ndq+rmPlaZSkJFCRbZ7I3KO8IPX6pl5mewWxdYL1JDI06hHu/7epn3h5tlJV9w==",
"dev": true
},
"node_modules/svelte-intersection-observer-action": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/svelte-intersection-observer-action/-/svelte-intersection-observer-action-0.0.5.tgz",
"integrity": "sha512-d5WVlE3Dpxx554cOYBm9BkIfba0+/r1O1yKuekQH/ScXW78mNB+R7A/LSfmGTFBNHvqjPZ9Vehfx1iIJti1saw==",
"dev": true
},
"node_modules/svelte-inview": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/svelte-inview/-/svelte-inview-4.0.2.tgz",
@ -8817,12 +8800,6 @@
}
}
},
"node_modules/svelte-progress-bar": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/svelte-progress-bar/-/svelte-progress-bar-3.0.2.tgz",
"integrity": "sha512-OpHgSg7ebvCtzA1mnUzOSftG7qcJ6llbopGuLCQ3Usz8RnFMm5S4DcclaaSDHQ33NJ8s+SrN/sEVuKZIDF7asw==",
"dev": true
},
"node_modules/svelte-sonner": {
"version": "0.3.27",
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.27.tgz",
@ -8832,12 +8809,6 @@
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.1"
}
},
"node_modules/svelte-tags-input": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/svelte-tags-input/-/svelte-tags-input-6.0.1.tgz",
"integrity": "sha512-3X5qomFSXe6E8H7Lq0oce7096tr6u06pWvTNgNoeNVIXCrRLxRYOk4Ujkte7z5WaAit47EUsZZP1TSJ2HR9ixA==",
"dev": true
},
"node_modules/svelte-tiny-virtual-list": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/svelte-tiny-virtual-list/-/svelte-tiny-virtual-list-2.1.2.tgz",

View File

@ -66,21 +66,16 @@
"svelte-check": "^3.6.9",
"svelte-echarts": "^1.0.0-rc3",
"svelte-french-toast": "^1.2.0",
"svelte-intersection-observer": "^1.0.0",
"svelte-intersection-observer-action": "^0.0.5",
"svelte-inview": "^4.0.2",
"svelte-lazy": "^1.2.11",
"svelte-lightweight-charts": "^2.2.0",
"svelte-loading-spinners": "^0.3.6",
"svelte-preprocess": "^5.1.4",
"svelte-progress-bar": "^3.0.2",
"svelte-sonner": "^0.3.27",
"svelte-tags-input": "^6.0.1",
"svelte-tiny-virtual-list": "^2.1.2",
"tailwind-merge": "^2.5.2",
"tailwind-variants": "^0.2.1",
"tailwindcss": "^4.0.9",
"tslib": "^2.7.0",
"typescript": "^5.4.5",
"util": "^0.12.5",
"uuid": "^10.0.0",

View File

@ -1,199 +0,0 @@
<!--
@component
Generates an HTML circle pack chart using [d3-hierarchy](https://github.com/d3/d3-hierarchy).
-->
<script>
import { stratify, pack, hierarchy } from "d3-hierarchy";
import { getContext } from "svelte";
import { format } from "d3-format";
import { screenWidth } from "$lib/store";
const { width, height, data } = getContext("LayerCake");
/** @type {String} [idKey='id'] - The key on each object where the id value lives. */
export let idKey = "id";
/** @type {String} [parentKey] - Set this if you want to define one parent circle. This will give you a [nested](https://layercake.graphics/example/CirclePackNested) graphic versus a [grouping of circles](https://layercake.graphics/example/CirclePack). */
export let parentKey = undefined;
/** @type {String} [valueKey='value'] - The key on each object where the data value lives. */
export let valueKey = "value";
/** @type {Function} [labelVisibilityThreshold=r => r > 25] - By default, only show the text inside a circle if its radius exceeds a certain size. Provide your own function for different behavior. */
export let labelVisibilityThreshold = (r) => r > 25;
/** @type {String} [stroke='#999'] - The circle's stroke color. */
export let stroke = "#000";
/** @type {String} [textColor='#333'] - The label text color. */
export let textColor = "#000";
/** @type {String} [textStroke='#000'] - The label text's stroke color. */
export let textStroke = "#000";
/** @type {Number} [textStrokeWidth=0] - The label text's stroke width, in pixels. */
export let textStrokeWidth = 0;
/** @type {Function} [sortBy=(a, b) => b.value - a.value] - The order in which circle's are drawn. Sorting on the `depth` key is also a popular choice. */
export let sortBy = (a, b) => b.value - a.value; // 'depth' is also a popular choice
/** @type {Number} [spacing=0] - Whitespace padding between each circle, in pixels. */
export let spacing = 0;
/* --------------------------------------------
* This component will automatically group your data
* into one group if no `parentKey` was passed in.
* Stash $data here so we can add our own parent
* if there's no `parentKey`
*/
let parent = {};
$: dataset = $data;
$: if (parentKey === undefined) {
parent = { [idKey]: "all" };
dataset = [...dataset, parent];
}
$: stratifier = stratify()
.id((d) => d[idKey])
.parentId((d) => {
if (d[idKey] === parent[idKey]) return "";
return d[parentKey] || parent[idKey];
});
$: packer = pack().size([$width, $height]).padding(spacing);
$: stratified = stratifier(dataset);
$: root = hierarchy(stratified)
.sum((d, i) => {
return d.data[valueKey] || 1;
})
.sort(sortBy);
$: packed = packer(root);
$: descendants = packed.descendants();
$: ballSize = $screenWidth < 1024 ? 2 : 3;
const titleCase = (d) => d.replace(/^\w/, (w) => w.toUpperCase());
const commas = format(",");
</script>
<div class="circle-pack" data-has-parent-key={parentKey !== undefined}>
{#each descendants as d, index}
<div
class="circle-group"
data-id={d.data.id}
data-visible={labelVisibilityThreshold(d.r)}
>
<div
class="circle"
style="left:{d.x}px;top:{d.y}px;width:{d.r * ballSize}px;height:{d.r *
ballSize}px;background-color:{index === 1 && d.data.id === 'puts'
? '#FF2F1F'
: index === 1 && d.data.id === 'calls'
? '#00FC50'
: '#1E1E1E'}; border: 0px solid #000;"
/>
<div
class="text-group"
style="
color:{textColor};
text-shadow:
-{textStrokeWidth}px -{textStrokeWidth}px 0 {textStroke},
{textStrokeWidth}px -{textStrokeWidth}px 0 {textStroke},
-{textStrokeWidth}px {textStrokeWidth}px 0 {textStroke},
{textStrokeWidth}px {textStrokeWidth}px 0 {textStroke};
left:{d.x}px;
top:{d.y - (labelVisibilityThreshold(d.r) ? 0 : d.r + 4)}px;
"
>
<div class="flex flex-col items-center m-auto">
{#if d.data.data[valueKey]}
<div
class="{index === 1
? 'text-xl font-semibold text-black'
: 'text-[1rem] font-semibold text-[#6B6C70]'} text-center"
>
{commas(d.data.data[valueKey])}%
</div>
{/if}
<div
class="{index === 1
? 'text-xl font-semibold text-black'
: 'text-sm font-semibold text-[#6B6C70]'} text-center"
>
{titleCase(d.data.id)}
</div>
</div>
</div>
</div>
{/each}
</div>
<style>
.circle-pack {
position: relative;
width: 100%;
height: 100%;
}
.circle,
.text-group {
position: absolute;
}
.circle {
transform: translate(-50%, -50%);
}
/* Hide the root node if we want, useful if we are creating our own root */
.circle-pack[data-has-parent-key="false"] .circle-group[data-id="all"] {
display: none;
}
/* .circle-group:hover {
z-index: 9999;
} */
.circle-group[data-visible="false"] .text-group {
display: none;
padding: 4px 7px;
background: #fff;
border: 1px solid #ccc;
transform: translate(-50%, -100%);
top: -4px;
}
.circle-group[data-visible="false"]:hover .text-group {
z-index: 999;
display: block !important;
/* On hover, set the text color to black and eliminate the shadow */
text-shadow: none !important;
color: #000 !important;
}
.circle-group[data-visible="false"]:hover .circle {
border-color: #000 !important;
}
.text-group {
width: auto;
top: 50%;
left: 50%;
text-align: center;
transform: translate(-50%, -50%);
white-space: nowrap;
pointer-events: none;
cursor: pointer;
line-height: 20px;
}
.text {
width: 100%;
font-size: 19px;
/* text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff; */
}
.text.value {
font-size: 15px;
}
.circle {
border-radius: 50%;
top: 0;
left: 0;
}
</style>

View File

@ -3,6 +3,7 @@
import InfoModal from "$lib/components/InfoModal.svelte";
import { abbreviateNumber, removeCompanyStrings } from "$lib/utils";
import highcharts from "$lib/highcharts.ts";
import { goto } from "$app/navigation";
export let data;
export let rawData = [];
@ -293,7 +294,7 @@
{#if data?.user?.tier !== "Pro" && i > 0}
<button
on:click={() => goto("/pricing")}
class="group relative z-1 rounded-full w-1/2 min-w-24 md:w-auto px-5 py-1"
class="cursor-pointer group relative z-1 rounded-full w-1/2 min-w-24 md:w-auto px-5 py-1"
>
<span class="relative text-sm block font-semibold">
{item.title}
@ -311,7 +312,7 @@
{:else}
<button
on:click={() => changeTimePeriod(i)}
class="group relative z-1 rounded-full w-1/2 min-w-24 md:w-auto px-5 py-1 {activeIdx ===
class="cursor-pointer group relative z-1 rounded-full w-1/2 min-w-24 md:w-auto px-5 py-1 {activeIdx ===
i
? 'z-0'
: ''} "
@ -371,7 +372,7 @@
<td
class="text-white text-sm sm:text-[1rem] text-right whitespace-nowrap"
>
{item?.totalVolume}
{abbreviateNumber(item?.totalVolume)}
</td>
<td

View File

@ -20,7 +20,7 @@
let activeIdx = 0;
const tabs = [
{
title: "Annual",
title: "Daily",
},
{
title: "Quarterly",
@ -31,47 +31,31 @@
(a, b) => new Date(b?.date) - new Date(a?.date),
);
tableList = filterByPeriod([...tableList], activeIdx);
function changeTimePeriod(i) {
activeIdx = i;
tableList = rawData?.sort((a, b) => new Date(b?.date) - new Date(a?.date));
tableList = filterByPeriod([...tableList], i);
if (activeIdx === 1) {
tableList = filterByPeriod([...tableList]);
}
}
function filterByPeriod(data, period) {
function filterByPeriod(data) {
if (!Array.isArray(data) || data.length === 0) return [];
if (period === 0) {
// Annual: one result per year.
const seenYears = new Set();
return data.filter((item) => {
const dt = new Date(item.date);
const year = dt.getFullYear();
if (!seenYears.has(year)) {
seenYears.add(year);
return true;
}
return false;
});
} else if (period === 1) {
// Quarterly: one result per year-quarter.
const seenPeriods = new Set();
return data.filter((item) => {
const dt = new Date(item.date);
const year = dt.getFullYear();
const quarter = Math.floor(dt.getMonth() / 3) + 1; // Quarter 1 to 4
const key = `${year}-Q${quarter}`;
if (!seenPeriods.has(key)) {
seenPeriods.add(key);
return true;
}
return false;
});
}
return [];
// Quarterly: one result per year-quarter.
const seenPeriods = new Set();
return data.filter((item) => {
const dt = new Date(item.date);
const year = dt.getFullYear();
const quarter = Math.floor(dt.getMonth() / 3) + 1; // Quarter 1 to 4
const key = `${year}-Q${quarter}`;
if (!seenPeriods.has(key)) {
seenPeriods.add(key);
return true;
}
return false;
});
}
function getPlotOptions() {
@ -293,7 +277,7 @@
{#if data?.user?.tier !== "Pro" && i > 0}
<button
on:click={() => goto("/pricing")}
class="group relative z-1 rounded-full w-1/2 min-w-24 md:w-auto px-5 py-1"
class="cursor-pointer group relative z-1 rounded-full w-1/2 min-w-24 md:w-auto px-5 py-1"
>
<span class="relative text-sm block font-semibold">
{item.title}
@ -311,7 +295,7 @@
{:else}
<button
on:click={() => changeTimePeriod(i)}
class="group relative z-1 rounded-full w-1/2 min-w-24 md:w-auto px-5 py-1 {activeIdx ===
class="cursor-pointer group relative z-1 rounded-full w-1/2 min-w-24 md:w-auto px-5 py-1 {activeIdx ===
i
? 'z-0'
: ''} "
@ -352,9 +336,7 @@
<tbody>
{#each tableList as item, index}
<!-- row -->
<tr
class="sm:hover:bg-[#245073] sm:hover:bg-opacity-[0.2] odd:bg-odd border-b border-gray-800"
>
<tr class="odd:bg-odd">
<td
class="text-white font-medium text-sm sm:text-[1rem] whitespace-nowrap"
>

View File

@ -1,119 +0,0 @@
<!--
@component
Generates an SVG x-axis. This component is also configured to detect if your x-scale is an ordinal scale. If so, it will place the markers in the middle of the bandwidth.
-->
<script>
import { getContext } from 'svelte';
const { width, height, xScale, yRange } = getContext('LayerCake');
/** @type {Boolean} [tickMarks=false] - Show a vertical mark for each tick. */
export let tickMarks = false;
/** @type {Boolean} [gridlines=true] - Show gridlines extending into the chart area. */
export let gridlines = true;
/** @type {Number} [tickMarkLength=6] - The length of the tick mark. */
export let tickMarkLength = 6;
/** @type {Boolean} [baseline=false] Show a solid line at the bottom. */
export let baseline = false;
/** @type {Boolean} [snapLabels=false] - Instead of centering the text labels on the first and the last items, align them to the edges of the chart. */
export let snapLabels = false;
/** @type {Function} [format=d => d] - A function that passes the current tick value and expects a nicely formatted value in return. */
export let format = d => d;
/** @type {Number|Array|Function} [ticks] - If this is a number, it passes that along to the [d3Scale.ticks](https://github.com/d3/d3-scale) function. If this is an array, hardcodes the ticks to those values. If it's a function, passes along the default tick values and expects an array of tick values in return. If nothing, it uses the default ticks supplied by the D3 function. */
export let ticks = undefined;
/** @type {Number} [tickGutter=0] - The amount of whitespace between the start of the tick and the chart drawing area (the yRange min). */
export let tickGutter = 0;
/** @type {Number} [dx=0] - Any optional value passed to the `dx` attribute on the text label. */
export let dx = 0;
/** @type {Number} [dy=12] - Any optional value passed to the `dy` attribute on the text label. */
export let dy = 12;
function textAnchor(i, sl) {
if (sl === true) {
if (i === 0) {
return 'start';
}
if (i === tickVals.length - 1) {
return 'end';
}
}
return 'middle';
}
$: tickLen = tickMarks === true ? tickMarkLength ?? 6 : 0;
$: isBandwidth = typeof $xScale.bandwidth === 'function';
$: tickVals = Array.isArray(ticks)
? ticks
: isBandwidth
? $xScale.domain()
: typeof ticks === 'function'
? ticks($xScale.ticks())
: $xScale.ticks(ticks);
$: halfBand = isBandwidth ? $xScale.bandwidth() / 2 : 0;
</script>
<g class="axis x-axis" class:snapLabels>
{#each tickVals as tick, i (tick)}
{#if baseline === true}
<line class="baseline" y1={$height} y2={$height} x1="0" x2={$width} />
{/if}
<g class="tick tick-{i}" transform="translate({$xScale(tick)},{Math.max(...$yRange)})">
{#if gridlines === true}
<line class="gridline" x1={halfBand} x2={halfBand} y1={-$height} y2="0" />
{/if}
{#if tickMarks === true}
<line
class="tick-mark"
x1={halfBand}
x2={halfBand}
y1={tickGutter}
y2={tickGutter + tickLen}
/>
{/if}
<text x={halfBand} y={tickGutter + tickLen} {dx} {dy} text-anchor={textAnchor(i, snapLabels)}
>{format(tick)}</text
>
</g>
{/each}
</g>
<style>
.tick {
font-size: 11px;
}
line,
.tick line {
stroke: #aaa;
stroke-dasharray: 2;
}
.tick text {
fill: #666;
}
.tick .tick-mark,
.baseline {
stroke-dasharray: 0;
}
/* This looks slightly better */
.axis.snapLabels .tick:last-child text {
transform: translateX(3px);
}
.axis.snapLabels .tick.tick-0 text {
transform: translateX(-3px);
}
</style>

View File

@ -1,119 +0,0 @@
<!--
@component
Generates an SVG y-axis. This component is also configured to detect if your y-scale is an ordinal scale. If so, it will place the tickMarks in the middle of the bandwidth.
-->
<script>
import { getContext } from 'svelte';
const { xRange, yScale, width } = getContext('LayerCake');
/** @type {Boolean} [tickMarks=false] - Show marks next to the tick label. */
export let tickMarks = false;
/** @type {String} [labelPosition='even'] - Whether the label sits even with its value ('even') or sits on top ('above') the tick mark. Default is 'even'. */
export let labelPosition = 'even';
/** @type {Boolean} [snapBaselineLabel=false] - When labelPosition='even', adjust the lowest label so that it sits above the tick mark. */
export let snapBaselineLabel = false;
/** @type {Boolean} [gridlines=true] - Show gridlines extending into the chart area. */
export let gridlines = true;
/** @type {Number} [tickMarkLength=undefined] - The length of the tick mark. If not set, becomes the length of the widest tick. */
export let tickMarkLength = undefined;
/** @type {Function} [format=d => d] - A function that passes the current tick value and expects a nicely formatted value in return. */
export let format = d => d;
/** @type {Number|Array|Function} [ticks=4] - If this is a number, it passes that along to the [d3Scale.ticks](https://github.com/d3/d3-scale) function. If this is an array, hardcodes the ticks to those values. If it's a function, passes along the default tick values and expects an array of tick values in return. */
export let ticks = 4;
/** @type {Number} [tickGutter=0] - The amount of whitespace between the start of the tick and the chart drawing area (the xRange min). */
export let tickGutter = 2;
/** @type {Number} [dx=0] - Any optional value passed to the `dx` attribute on the text label. */
export let dx = 0;
/** @type {Number} [dy=0] - Any optional value passed to the `dy` attribute on the text label. */
export let dy = 0;
/** @type {Number} [charPixelWidth=7.25] - Used to calculate the widest label length to offset labels. Adjust if the automatic tick length doesn't look right because you have a bigger font (or just set `tickMarkLength` to a pixel value). */
export let charPixelWidth = 7.25;
$: isBandwidth = typeof $yScale.bandwidth === 'function';
$: tickVals = Array.isArray(ticks)
? ticks
: isBandwidth
? $yScale.domain()
: typeof ticks === 'function'
? ticks($yScale.ticks())
: $yScale.ticks(ticks);
function calcStringLength(sum, val) {
if (val === ',' || val === '.') return sum + charPixelWidth * 0.5;
return sum + charPixelWidth;
}
$: tickLen =
tickMarks === true
? labelPosition === 'above'
? tickMarkLength ?? widestTickLen
: tickMarkLength ?? 6
: 0;
$: widestTickLen = Math.max(
10,
Math.max(...tickVals.map(d => format(d).toString().split('').reduce(calcStringLength, 0)))
);
$: x1 = -tickGutter - (labelPosition === 'above' ? widestTickLen : tickLen);
$: y = isBandwidth ? $yScale.bandwidth() / 2 : 0;
$: maxTickValPx = Math.max(...tickVals.map($yScale));
</script>
<g class="axis y-axis">
{#each tickVals as tick (tick)}
{@const tickValPx = $yScale(tick)}
<g class="tick tick-{tick}" transform="translate({$xRange[0]}, {tickValPx})">
{#if gridlines === true}
<line class="gridline" {x1} x2={$width} y1={y} y2={y}></line>
{/if}
{#if tickMarks === true}
<line class="tick-mark" {x1} x2={x1 + tickLen} y1={y} y2={y}></line>
{/if}
<text
x={x1}
{y}
dx={dx + (labelPosition === 'even' ? -3 : 0)}
text-anchor={labelPosition === 'above' ? 'start' : 'end'}
dy={dy +
(labelPosition === 'above' || (snapBaselineLabel === true && tickValPx === maxTickValPx)
? -3
: 4)}>{format(tick)}</text
>
</g>
{/each}
</g>
<style>
.tick {
font-size: 13px;
}
.tick line {
stroke: #fff;
}
.tick .gridline {
stroke-dasharray: 2;
}
.tick text {
fill: #fff;
}
.tick.tick-0 line {
stroke-dasharray: 0;
}
</style>

View File

@ -1,26 +0,0 @@
<!--
@component
Generates an SVG bar chart.
-->
<script>
import { getContext } from 'svelte';
const { data, xGet, yGet, xScale, yScale } = getContext('LayerCake');
/** @type {String} [fill='#3B82F6'] - The shape's fill color. This is technically optional because it comes with a default value but you'll likely want to replace it with your own color. */
export let fill = '#fff';
</script>
<g class="bar-group">
{#each $data as d, i}
<rect
class="group-rect"
data-id={i}
x={$xScale.range()[0]}
y={$yGet(d)}
height={$yScale.bandwidth()}
width={$xGet(d)}
{fill}
></rect>
{/each}
</g>