[READ-ONLY] Mirror of https://github.com/probablykasper/starchart. GitHub star history graph starchart.kasper.space
chart github github-api github-star graph history star star-history stargazers stars
0

Configure Feed

Select the types of activity you want to include in your feed.

Improve performance

Kasper (Mar 1, 2023, 11:39 AM +0100) 9924b2ab 7f634c47

+360 -389
+45 -69
src/routes/+page.svelte
··· 1 - <script lang="ts" context="module"> 2 - import type { UTCTimestamp } from 'lightweight-charts' 3 - 4 - export type DataPoint = { t: UTCTimestamp; v: number } 5 - export type SeriesData = { 6 - name: string 7 - data: DataPoint[] 8 - color: number 9 - /** Loading if absent */ 10 - final?: DataPoint 11 - } 12 - </script> 13 - 14 1 <script lang="ts"> 15 2 import { fly, slide } from 'svelte/transition' 16 3 import { fetchStargazersPage } from './github' 17 4 import { onMount } from 'svelte' 18 - import { getNextColorIndex } from './Chart.svelte' 19 5 import '../app.sass' 20 6 import Nav from './Nav.svelte' 21 7 import LabeledChart from './LabeledChart.svelte' 8 + import { getNextColorIndex, newChart, type Chart } from './chart' 9 + import type { UTCTimestamp } from 'lightweight-charts' 22 10 23 11 let [owner, repo] = ['', ''] 24 12 25 - type Json = { 26 - series: SeriesData[] 27 - v: number 28 - expiry: number 29 - } 30 - /** Used to invalidate old localStorage */ 31 - const jsonTypeVersion = 1 32 - 33 - let series: SeriesData[] = [] 34 - onMount(() => { 35 - try { 36 - const seriesLocalStorage = JSON.parse(localStorage.getItem('starchart-series') || '[]') 37 - if (seriesLocalStorage?.v !== jsonTypeVersion || seriesLocalStorage?.expiry < Date.now()) { 38 - localStorage.removeItem('starchart-series') 39 - } else { 40 - series = (seriesLocalStorage as Json).series 41 - } 42 - } catch (_e) { 43 - // ignore 44 - } 45 - }) 13 + let chart: Chart 46 14 47 15 async function getStargazers(owner: string, repo: string) { 48 - for (const serie of series) { 49 - if (serie.name === `${owner}/${repo}`) { 16 + for (const line of $chart.lines) { 17 + if (line.name === `${owner}/${repo}`) { 50 18 return // already added 51 19 } 52 20 } 53 21 let count = 0 54 22 let endCursor: string | undefined 55 - const newSerie: SeriesData = { 23 + const line = chart.addLine({ 56 24 name: `${owner}/${repo}`, 57 25 color: getNextColorIndex(), 58 26 data: [], 59 - } 27 + }) 60 28 let totalCount = 0 61 - series.push(newSerie) // don't trigger store update yet 62 29 do { 63 - if (!series.find((serie) => serie.name === newSerie.name)) { 64 - // abort 65 - return 30 + if (line.deleted) { 31 + return // abort 66 32 } 67 33 const { error, stargazers } = await fetchStargazersPage(owner, repo, 'forward', endCursor) 68 34 if (!stargazers) { 69 35 addError(error) 70 - series = series.filter((serie) => serie.name !== newSerie.name) 36 + chart.deleteLine(line) 71 37 return 72 38 } 73 39 ··· 85 51 v: count, 86 52 } 87 53 }) 88 - 89 - newSerie.data = [...newSerie.data, ...newData] 90 - series = series 54 + chart.appendLineData(line, newData) 91 55 } while (endCursor) 92 - newSerie.final = { 56 + line.final = { 93 57 t: Math.ceil(new Date().getTime() / 1000) as UTCTimestamp, 94 58 v: totalCount, 95 59 } 96 - save() 97 - } 98 - 99 - function save() { 100 - if (series.length > 0) { 101 - const day = 1000 * 60 * 60 * 24 102 - const json: Json = { 103 - series: series.filter((serie) => !!serie.final), 104 - v: jsonTypeVersion, 105 - expiry: Date.now() + day * 1, 106 - } 107 - localStorage.setItem('starchart-series', JSON.stringify(json)) 108 - } else { 109 - localStorage.removeItem('starchart-series') 110 - } 60 + chart.appendLineData(line, [line.final]) 61 + chart.save() 111 62 } 112 63 113 64 let errors: { id: number; msg: string }[] = [] 114 - 115 65 function addError(msg: string) { 116 66 const id = Math.random() 117 67 errors.push({ id, msg }) 118 68 errors = errors 119 69 return id 120 70 } 71 + 72 + let width: number 73 + let height: number 74 + $: if (width) { 75 + height = Math.round(width * 0.6) 76 + } 77 + 78 + let container: HTMLDivElement 79 + onMount(() => { 80 + chart = newChart(container, { width, height }) 81 + }) 82 + $: $chart?.instance.applyOptions({ 83 + width, 84 + height, 85 + }) 121 86 </script> 122 87 123 88 <Nav bind:owner bind:repo onSubmit={() => getStargazers(owner, repo)} /> ··· 151 116 </div> 152 117 {/each} 153 118 154 - <LabeledChart bind:series {save} /> 119 + <div class="chart" bind:clientWidth={width}> 120 + {#if $chart && $chart.lines.length > 0} 121 + <LabeledChart {chart} /> 122 + {/if} 123 + <div bind:this={container} class:hidden={!$chart || $chart.lines.length === 0} /> 124 + </div> 155 125 156 126 <style lang="sass"> 157 127 .error-container 158 128 padding-bottom: 1rem 159 129 .error 160 - border: 1px solid hsla(0, 100%, 50%, 0.5) 130 + border: 2px solid hsla(0, 100%, 50%, 0.5) 161 131 background-color: hsla(0, 100%, 50%, 0.25) 162 - color: hsl(0, 100%, 66%) 132 + // color: hsl(0, 100%, 66%) 163 133 border-radius: 8px 164 134 padding: 0.75rem 1rem 135 + font-weight: 500 165 136 margin: 0rem auto 166 137 max-width: 650px 167 - font-size: 0.9rem 138 + font-size: 0.875rem 168 139 position: relative 169 140 text-align: initial 170 141 button ··· 175 146 position: absolute 176 147 top: 0px 177 148 right: 0px 178 - padding: 0.2rem 149 + padding: 0.2rem 0.25rem 179 150 &:hover 180 151 opacity: 0.7 152 + .chart 153 + width: 100% 154 + padding-bottom: 50px 155 + .hidden 156 + display: none 181 157 </style>
+11 -183
src/routes/Chart.svelte
··· 1 - <script lang="ts" context="module"> 2 - import { TinyColor } from '@ctrl/tinycolor' 3 - 4 - export const hexColors = ['#E91E63', '#0064fa', '#ffdd00', '#FF9800', '#08fd96', '#FFFFFF'] 5 - 6 - let nextColorIndex = 0 7 - export function getNextColorIndex() { 8 - const value = nextColorIndex 9 - nextColorIndex = (nextColorIndex + 1) % hexColors.length 10 - return value 11 - } 12 - </script> 13 - 14 1 <script lang="ts"> 15 - import type { SeriesData } from './+page.svelte' 16 - import type { 17 - UTCTimestamp, 18 - IChartApi, 19 - ISeriesApi, 20 - SeriesDataItemTypeMap, 21 - } from 'lightweight-charts' 2 + import type { UTCTimestamp } from 'lightweight-charts' 22 3 import { ColorType } from 'lightweight-charts' 23 - import { onDestroy } from 'svelte' 4 + import type { Chart } from './chart' 24 5 25 - export let container: HTMLDivElement 26 - export let chart: IChartApi 27 - export let data: SeriesData[] 28 - export let width: number 29 - export let height: number 6 + export let chart: Chart 30 7 31 - $: chart.applyOptions({ 32 - width, 33 - height, 34 - }) 35 - 36 - const topColors = hexColors.map((hexColor) => { 37 - const color = new TinyColor(hexColor) 38 - color.setAlpha(0.5) 39 - return color.saturate(100).toRgbString() 40 - }) 41 - const bottomColors = hexColors.map((hexColor) => { 42 - const color = new TinyColor(hexColor) 43 - color.setAlpha(0) 44 - return color.darken(0).toRgbString() 45 - }) 46 - 47 - chart.applyOptions({ 48 - height, 49 - width, 8 + $chart.instance.applyOptions({ 50 9 layout: { 51 10 background: { type: ColorType.Solid, color: 'transparent' }, 52 11 textColor: '#FFFFFF', ··· 89 48 }, 90 49 }) 91 50 92 - export const seriesList: ISeriesApi<'Area'>[] = [] 93 - 94 - export function removeSeries(i: number) { 95 - chart.removeSeries(seriesList[i]) 96 - seriesList.splice(i, 1) 97 - } 98 - 99 - export function setSeriesVisible(i: number, visible: boolean) { 100 - seriesList[i].applyOptions({ visible }) 101 - } 102 - 103 - function toLineSeriesData(series: SeriesData): SeriesDataItemTypeMap['Area'][] { 104 - const dataPoints: { date: Date; value?: number }[] = [] 105 - 106 - const fullSeries = series.final ? [...series.data, series.final] : series.data 107 - 108 - for (const dataPoint of fullSeries) { 109 - const dt = new Date(dataPoint.t * 1000) 110 - const date = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate()) 111 - 112 - let lastDataPoint = dataPoints[dataPoints.length - 1] 113 - if (lastDataPoint) { 114 - while (lastDataPoint.date < date) { 115 - dataPoints.push({ 116 - date: new Date( 117 - lastDataPoint.date.getFullYear(), 118 - lastDataPoint.date.getMonth(), 119 - lastDataPoint.date.getDate() + 1 120 - ), 121 - value: lastDataPoint.value, 122 - }) 123 - lastDataPoint = dataPoints[dataPoints.length - 1] 124 - } 125 - } 126 - if (lastDataPoint && lastDataPoint.date.getTime() === date.getTime()) { 127 - dataPoints[dataPoints.length - 1].value = dataPoint.v 128 - } else { 129 - dataPoints.push({ 130 - date: date, 131 - value: dataPoint.v, 132 - }) 133 - } 134 - } 135 - 136 - return dataPoints.map((dataPoint) => { 137 - const dp: SeriesDataItemTypeMap['Area'] = { 138 - time: (dataPoint.date.getTime() / 1000) as UTCTimestamp, 139 - value: dataPoint.value, 140 - } 141 - return dp 142 - }) 143 - } 144 - 145 - let fillerSeries = chart.addAreaSeries({ 146 - priceScaleId: '', 147 - visible: false, 148 - }) 149 - function getFiller(data: SeriesData[]) { 150 - let lowestDate = (Date.now() / 1000) as UTCTimestamp 151 - for (const series of data) { 152 - const dataPoint = series.data[0] 153 - if (dataPoint && dataPoint.t < lowestDate) { 154 - lowestDate = dataPoint.t 155 - } 156 - } 157 - const dt = new Date(lowestDate * 1000) 158 - const date = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate()) 159 - 160 - let dates = [date] 161 - while (dates[dates.length - 1] < new Date()) { 162 - dates.push( 163 - new Date( 164 - dates[dates.length - 1].getFullYear(), 165 - dates[dates.length - 1].getMonth(), 166 - dates[dates.length - 1].getDate() + 1 167 - ) 168 - ) 169 - } 170 - fillerSeries.setData( 171 - dates.map((date) => { 172 - return { 173 - time: (date.getTime() / 1000) as UTCTimestamp, 174 - } 175 - }) 176 - ) 177 - } 178 - 179 - $: update(data) 180 - function update(data: SeriesData[]) { 181 - let willExpand = data.length > seriesList.length 182 - for (let i = 0; i < data.length; i++) { 183 - if (!seriesList[i]) { 184 - const series = chart.addAreaSeries({ 185 - priceLineVisible: false, 186 - topColor: topColors[data[i].color], 187 - bottomColor: bottomColors[data[i].color], 188 - lineColor: hexColors[data[i].color], 189 - priceFormat: { 190 - type: 'custom', 191 - formatter: (price: number) => { 192 - return Math.round(price) 193 - }, 194 - }, 195 - }) 196 - seriesList.push(series) 197 - } 198 - seriesList[i].setData(toLineSeriesData(data[i])) 199 - } 200 - if (data[data.length - 1]) { 201 - const lastColor = data[data.length - 1].color 202 - const lastColorIndex = hexColors.findIndex((color) => color === hexColors[lastColor]) 203 - nextColorIndex = (lastColorIndex + 1) % hexColors.length 204 - } 205 - getFiller(data) 206 - if (willExpand) { 207 - resetZoom() 208 - } 209 - } 210 - 211 51 let toolTipValues: { name: string; value: number }[] = [] 212 52 let toolTipDateStr = 0 213 53 let toolTipLeft = 0 ··· 215 55 let tooltipVisible = false 216 56 217 57 // update tooltip 218 - chart.subscribeCrosshairMove((param) => { 58 + $chart.instance.subscribeCrosshairMove((param) => { 219 59 if ( 220 60 param.point === undefined || 221 61 !param.time || 222 62 param.point.x < 0 || 223 - param.point.x > container.clientWidth || 63 + param.point.x > $chart.container.clientWidth || 224 64 param.point.y < 0 || 225 - param.point.y > container.clientHeight 65 + param.point.y > $chart.container.clientHeight 226 66 ) { 227 67 tooltipVisible = false 228 68 } else { ··· 230 70 // thus it will be YYYY-MM-DD 231 71 toolTipDateStr = param.time as UTCTimestamp 232 72 tooltipVisible = true 233 - for (let i = 0; i < seriesList.length; i++) { 73 + for (let i = 0; i < $chart.lines.length; i++) { 234 74 toolTipValues = [] 235 - const lineData = param.seriesData.get(seriesList[i]) 75 + const lineData = param.seriesData.get($chart.lines[i].instance) 236 76 let v = 0 237 77 if (lineData && 'value' in lineData && lineData.value !== undefined) { 238 78 v = lineData.value ··· 242 82 return 243 83 } 244 84 toolTipValues.push({ 245 - name: data[i].name, 85 + name: $chart.lines[i].name, 246 86 value: v, 247 87 }) 248 88 } 249 89 } 250 90 }) 251 - 252 - resetZoom() 253 - 254 - function resetZoom() { 255 - chart.timeScale().fitContent() 256 - } 257 - 258 - onDestroy(() => { 259 - for (const series of seriesList) { 260 - chart.removeSeries(series) 261 - } 262 - }) 263 91 </script> 264 92 265 - <svelte:window on:resize={resetZoom} /> 93 + <svelte:window on:resize={chart.resetZoom} /> 266 94 267 95 <div class="tooltip" class:hide={!tooltipVisible} style:left={toolTipLeft} style:top={toolTipTop}> 268 96 <div style="color: white">
-75
src/routes/ChartOld.svelte
··· 1 - <script lang="ts"> 2 - import type { SeriesDataItemTypeMap, UTCTimestamp, IChartApi } from 'lightweight-charts' 3 - import { Chart, LineSeries } from 'svelte-lightweight-charts' 4 - import type { SeriesData } from './+page.svelte' 5 - 6 - let chart: IChartApi | null = null 7 - 8 - export let data: SeriesData[] 9 - function toLineSeriesData(series: SeriesData): SeriesDataItemTypeMap['Line'][] { 10 - const dataPoints: { date: Date; value?: number }[] = [] 11 - for (const dataPoint of series.data) { 12 - const dt = new Date(dataPoint.t * 1000) 13 - const date = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate()) 14 - 15 - let lastDataPoint = dataPoints[dataPoints.length - 1] 16 - if (lastDataPoint) { 17 - while (lastDataPoint.date < date) { 18 - dataPoints.push({ 19 - date: new Date( 20 - lastDataPoint.date.getFullYear(), 21 - lastDataPoint.date.getMonth(), 22 - lastDataPoint.date.getDate() + 1 23 - ), 24 - // value: lastDataPoint.value, 25 - }) 26 - lastDataPoint = dataPoints[dataPoints.length - 1] 27 - } 28 - } 29 - if (lastDataPoint && lastDataPoint.date.getTime() === date.getTime()) { 30 - dataPoints[dataPoints.length - 1].value = dataPoint.v 31 - } else { 32 - dataPoints.push({ 33 - date: date, 34 - value: dataPoint.v, 35 - }) 36 - } 37 - } 38 - 39 - return dataPoints.map((dataPoint) => { 40 - const dp: SeriesDataItemTypeMap['Line'] = { 41 - time: (dataPoint.date.getTime() / 1000) as UTCTimestamp, 42 - value: dataPoint.value, 43 - } 44 - return dp 45 - }) 46 - } 47 - export let width: number 48 - export let height: number 49 - </script> 50 - 51 - <Chart 52 - ref={(ref) => (chart = ref)} 53 - {width} 54 - {height} 55 - layout={{ background: { color: 'transparent' }, textColor: '#FFFFFF' }} 56 - grid={{ 57 - vertLines: { 58 - visible: false, 59 - }, 60 - horzLines: { 61 - visible: false, 62 - }, 63 - }} 64 - crosshair={{ 65 - mode: 1, 66 - horzLine: { 67 - visible: false, 68 - labelVisible: false, 69 - }, 70 - }} 71 - > 72 - {#each data as series} 73 - <LineSeries data={toLineSeriesData(series)} color="red" /> 74 - {/each} 75 - </Chart>
+8 -8
src/routes/Label.svelte
··· 1 1 <script lang="ts"> 2 2 import { cubicOut } from 'svelte/easing' 3 3 import { fade, scale } from 'svelte/transition' 4 - import type { SeriesData } from './+page.svelte' 4 + import type { Line } from './chart' 5 + import { hexColors } from './color' 5 6 6 - export let hex: string 7 - export let serie: SeriesData 7 + export let line: Line 8 + $: hex = hexColors[line.color] 8 9 export let onDelete: () => void 9 10 let visible = true 10 11 export let onVisibleChange: (visible: boolean) => void ··· 36 37 onVisibleChange(visible) 37 38 }} 38 39 > 39 - {serie.name} 40 + {line.name} 40 41 </button> 41 - <button type="button" class="x" class:loading={!serie.final} on:click={onDelete} tabindex="-1"> 42 + <button type="button" class="x" class:loading={!line.final} on:click={onDelete} tabindex="-1"> 42 43 <svg 43 44 fill="currentColor" 44 45 width="18" ··· 53 54 d="m12 10.93 5.719-5.72c.146-.146.339-.219.531-.219.404 0 .75.324.75.749 0 .193-.073.385-.219.532l-5.72 5.719 5.719 5.719c.147.147.22.339.22.531 0 .427-.349.75-.75.75-.192 0-.385-.073-.531-.219l-5.719-5.719-5.719 5.719c-.146.146-.339.219-.531.219-.401 0-.75-.323-.75-.75 0-.192.073-.384.22-.531l5.719-5.719-5.72-5.719c-.146-.147-.219-.339-.219-.532 0-.425.346-.749.75-.749.192 0 .385.073.531.219z" 54 55 /></svg 55 56 > 56 - {#if !serie.final} 57 + {#if !line.final} 57 58 <div class="spinner" transition:fade={{ duration: 150 }}> 58 59 <div class="circle" /> 59 60 </div> ··· 79 80 svg 80 81 display: block 81 82 .hidden 82 - opacity: 0.5 83 + opacity: 0.6 83 84 text-decoration: line-through 84 85 border-style: dashed 85 86 button ··· 100 101 font-family: inherit 101 102 transition: all 50ms ease-out 102 103 padding-right: 0.2rem 103 - cursor: default 104 104 &:hover 105 105 text-decoration: line-through 106 106 .spinner
+11 -40
src/routes/LabeledChart.svelte
··· 1 1 <script lang="ts"> 2 - import { createChart, type IChartApi } from 'lightweight-charts' 3 - import { onMount } from 'svelte' 4 - import type { SeriesData } from './+page.svelte' 5 - import Chart, { hexColors } from './Chart.svelte' 2 + import ChartComponent from './Chart.svelte' 3 + import type { Chart } from './chart' 6 4 import Label from './Label.svelte' 7 5 8 - let width: number 9 - let height: number 10 - $: if (width) { 11 - height = Math.round(width * 0.6) 12 - } 13 - 14 - export let series: SeriesData[] = [] 15 - export let save: () => void 16 - 17 - let chart: IChartApi | null = null 18 - let container: HTMLDivElement 19 - onMount(() => { 20 - chart = createChart(container) 21 - }) 22 - 23 - let chartComponent: Chart 6 + export let chart: Chart 24 7 </script> 25 8 26 - <div class="series"> 27 - {#each series as serie, i (serie.name)} 9 + <div class="labels"> 10 + {#each $chart.lines as line (line.name)} 28 11 <Label 29 - hex={hexColors[serie.color]} 30 - {serie} 12 + {line} 31 13 onDelete={() => { 32 - series.splice(i, 1) 33 - chartComponent.removeSeries(i) 34 - series = series 35 - save() 14 + chart.deleteLine(line) 15 + chart.save() 36 16 }} 37 17 onVisibleChange={(visible) => { 38 - chartComponent.setSeriesVisible(i, visible) 18 + line.instance.applyOptions({ visible }) 39 19 }} 40 20 /> 41 21 {/each} 42 22 </div> 43 23 44 - <div class="chart" bind:clientWidth={width}> 45 - <div bind:this={container}> 46 - {#if chart && series.length > 0} 47 - <Chart bind:this={chartComponent} {container} {chart} data={series} {width} {height} /> 48 - {/if} 49 - </div> 50 - </div> 24 + <ChartComponent {chart} /> 51 25 52 26 <style lang="sass"> 53 - .series 27 + .labels 54 28 text-align: center 55 - .chart 56 - width: 100% 57 - padding-bottom: 50px 58 29 </style>
-1
src/routes/Modal.svelte
··· 16 16 function focusDefault(el: HTMLDivElement) { 17 17 if (!el.contains(document.activeElement)) { 18 18 el.focus() 19 - console.log('focd', el) 20 19 } 21 20 } 22 21 function close() {
+267
src/routes/chart.ts
··· 1 + import { 2 + createChart as createChartInstance, 3 + type ChartOptions, 4 + type DeepPartial, 5 + type IChartApi, 6 + type ISeriesApi, 7 + type SeriesDataItemTypeMap, 8 + type UTCTimestamp, 9 + } from 'lightweight-charts' 10 + import { writable } from 'svelte/store' 11 + import { bottomColors, hexColors, topColors } from './color' 12 + 13 + type DataPoint = { t: UTCTimestamp; v: number } 14 + type LineJson = { 15 + name: string 16 + data: DataPoint[] 17 + color: number 18 + /** Loading if absent */ 19 + final?: DataPoint 20 + } 21 + export interface Line extends LineJson { 22 + instance: ISeriesApi<'Area'> 23 + deleted: boolean 24 + } 25 + 26 + /** Used to invalidate old localStorage */ 27 + const jsonTypeVersion = 2 28 + type Json = { 29 + lines: LineJson[] 30 + v: number 31 + expiry: number 32 + } 33 + 34 + let nextColorIndex = 0 35 + export function getNextColorIndex() { 36 + const value = nextColorIndex 37 + nextColorIndex = (nextColorIndex + 1) % hexColors.length 38 + return value 39 + } 40 + 41 + function loadJsonLines(): LineJson[] { 42 + try { 43 + const seriesLocalStorage = JSON.parse(localStorage.getItem('starchart-series') || '[]') 44 + if (seriesLocalStorage?.v !== jsonTypeVersion || seriesLocalStorage?.expiry < Date.now()) { 45 + localStorage.removeItem('starchart-series') 46 + } else { 47 + return (seriesLocalStorage as Json).lines 48 + } 49 + } catch (_e) { 50 + // ignore 51 + } 52 + return [] 53 + } 54 + 55 + type ChartData = { 56 + instance: IChartApi 57 + lines: Line[] 58 + container: HTMLElement 59 + } 60 + export type Chart = ReturnType<typeof newChart> 61 + 62 + export function newChart(container: HTMLElement, options: DeepPartial<ChartOptions>) { 63 + const chart: ChartData = { 64 + instance: createChartInstance(container, options), 65 + container, 66 + lines: [], 67 + } 68 + const { subscribe, set } = writable(chart) 69 + 70 + const fillerLine = chart.instance.addAreaSeries({ 71 + priceScaleId: '', 72 + visible: false, 73 + }) 74 + 75 + function updateFiller(data: Line[]) { 76 + fillerLine.setData(getFiller(data)) 77 + } 78 + 79 + const store = { 80 + subscribe, 81 + 82 + addLine(lineJson: LineJson) { 83 + const series = chart.instance.addAreaSeries({ 84 + priceLineVisible: false, 85 + topColor: topColors[lineJson.color], 86 + bottomColor: bottomColors[lineJson.color], 87 + lineColor: hexColors[lineJson.color], 88 + priceFormat: { 89 + type: 'custom', 90 + formatter: (price: number) => { 91 + return Math.round(price) 92 + }, 93 + }, 94 + }) 95 + 96 + // series.removePriceLine() 97 + // series.createPriceLine({ 98 + // price: 800, 99 + // color: '#be1238', 100 + // lineWidth: 1, 101 + // lineStyle: LineStyle.Dashed, 102 + // }) 103 + 104 + if (lineJson.data.length >= 1) { 105 + const start = lineJson.data[0] 106 + const end = lineJson.final ?? lineJson.data[lineJson.data.length - 1] 107 + const chartData = toChartData(lineJson.data, start, end) 108 + series.setData(chartData) 109 + } 110 + const line: Line = { 111 + ...lineJson, 112 + instance: series, 113 + deleted: false, 114 + } 115 + chart.lines.push(line) 116 + updateFiller(chart.lines) 117 + if (lineJson.data.length >= 1) { 118 + store.resetZoom() 119 + } 120 + set(chart) 121 + return line 122 + }, 123 + 124 + deleteLine(line: Line) { 125 + chart.instance.removeSeries(line.instance) 126 + line.deleted = true 127 + chart.lines = chart.lines.filter((line) => !line.deleted) 128 + set(chart) 129 + }, 130 + 131 + appendLineData(line: Line, data: DataPoint[]) { 132 + const startTime = Date.now() 133 + if (data.length === 0) { 134 + return 135 + } 136 + const start = line.data[line.data.length - 1] ?? data[0] 137 + const end = line.final ?? data[data.length - 1] 138 + const chartData = toChartData(data, start, end) 139 + const fresh = line.data.length === 0 140 + 141 + if (fresh) { 142 + line.instance.setData(chartData) 143 + } else { 144 + for (const dataPoint of chartData) { 145 + line.instance.update(dataPoint) 146 + } 147 + } 148 + line.data.push(...data) 149 + updateFiller(chart.lines) 150 + if (fresh) { 151 + store.resetZoom() 152 + } 153 + console.log(Date.now() - startTime) 154 + 155 + set(chart) 156 + }, 157 + 158 + resetZoom() { 159 + chart.instance.timeScale().fitContent() 160 + }, 161 + 162 + save() { 163 + save(chart) 164 + }, 165 + } 166 + 167 + const jsonLines = loadJsonLines() 168 + for (const jsonLine of jsonLines) { 169 + store.addLine(jsonLine) 170 + } 171 + if (jsonLines[jsonLines.length - 1]) { 172 + const lastColor = jsonLines[jsonLines.length - 1].color 173 + const lastColorIndex = hexColors.findIndex((color) => color === hexColors[lastColor]) 174 + nextColorIndex = (lastColorIndex + 1) % hexColors.length 175 + } 176 + 177 + return store 178 + } 179 + 180 + function getFiller(data: Line[]) { 181 + let lowestDate = (Date.now() / 1000) as UTCTimestamp 182 + for (const series of data) { 183 + const dataPoint = series.data[0] 184 + if (dataPoint && dataPoint.t < lowestDate) { 185 + lowestDate = dataPoint.t 186 + } 187 + } 188 + const dt = new Date(lowestDate * 1000) 189 + const date = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate()) 190 + 191 + const dates = [date] 192 + while (dates[dates.length - 1] < new Date()) { 193 + dates.push( 194 + new Date( 195 + dates[dates.length - 1].getFullYear(), 196 + dates[dates.length - 1].getMonth(), 197 + dates[dates.length - 1].getDate() + 1 198 + ) 199 + ) 200 + } 201 + return dates.map((date) => { 202 + return { 203 + time: (date.getTime() / 1000) as UTCTimestamp, 204 + } 205 + }) 206 + } 207 + 208 + function save(chart: ChartData) { 209 + if (chart.lines.length === 0) { 210 + localStorage.removeItem('starchart-series') 211 + return 212 + } 213 + const day = 1000 * 60 * 60 * 24 214 + const jsonLines: LineJson[] = chart.lines.map((line) => ({ 215 + name: line.name, 216 + data: line.data, 217 + color: line.color, 218 + final: line.final, 219 + })) 220 + const json: Json = { 221 + lines: jsonLines, 222 + v: jsonTypeVersion, 223 + expiry: Date.now() + day * 1, 224 + } 225 + localStorage.setItem('starchart-series', JSON.stringify(json)) 226 + } 227 + 228 + type Series = SeriesDataItemTypeMap['Area'][] 229 + function toChartData(data: DataPoint[], start: DataPoint, final: DataPoint): Series { 230 + const dataPoints: { date: Date; value?: number }[] = [] 231 + 232 + for (const dataPoint of [start, ...data, final]) { 233 + const dt = new Date(dataPoint.t * 1000) 234 + const date = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate()) 235 + 236 + let lastDataPoint = (dataPoints as Partial<typeof dataPoints>)[dataPoints.length - 1] 237 + if (lastDataPoint) { 238 + while (lastDataPoint.date < date) { 239 + dataPoints.push({ 240 + date: new Date( 241 + lastDataPoint.date.getFullYear(), 242 + lastDataPoint.date.getMonth(), 243 + lastDataPoint.date.getDate() + 1 244 + ), 245 + value: lastDataPoint.value, 246 + }) 247 + lastDataPoint = dataPoints[dataPoints.length - 1] 248 + } 249 + } 250 + if (lastDataPoint && lastDataPoint.date.getTime() === date.getTime()) { 251 + dataPoints[dataPoints.length - 1].value = dataPoint.v 252 + } else { 253 + dataPoints.push({ 254 + date: date, 255 + value: dataPoint.v, 256 + }) 257 + } 258 + } 259 + 260 + return dataPoints.map((dataPoint) => { 261 + const dp: SeriesDataItemTypeMap['Area'] = { 262 + time: (dataPoint.date.getTime() / 1000) as UTCTimestamp, 263 + value: dataPoint.value, 264 + } 265 + return dp 266 + }) 267 + }
+13
src/routes/color.ts
··· 1 + import { TinyColor } from '@ctrl/tinycolor' 2 + 3 + export const hexColors = ['#E91E63', '#0064fa', '#ffdd00', '#FF9800', '#08fd96', '#FFFFFF'] 4 + export const topColors = hexColors.map((hexColor) => { 5 + const color = new TinyColor(hexColor) 6 + color.setAlpha(0.5) 7 + return color.saturate(100).toRgbString() 8 + }) 9 + export const bottomColors = hexColors.map((hexColor) => { 10 + const color = new TinyColor(hexColor) 11 + color.setAlpha(0) 12 + return color.darken(0).toRgbString() 13 + })
+5 -13
src/routes/github.ts
··· 1 1 import { Octokit } from '@octokit/core' 2 + import { GraphqlResponseError } from '@octokit/graphql' 2 3 import { PUBLIC_PAT } from '$env/static/public' 3 4 import { get, writable } from 'svelte/store' 4 5 import { browser } from '$app/environment' ··· 67 68 ) 68 69 69 70 const response = await responsePromise.catch((error) => { 70 - console.error(error) 71 - const response = error?.response 72 - const status = error?.response?.status 73 - console.log('status', status) 74 - const message = error?.response?.data?.message || '' 75 - if (!response) { 71 + if (error instanceof GraphqlResponseError && error.errors) { 76 72 return { 77 - error: typeof error.message === 'string' ? (error.message as string) : 'Unknown error', 73 + error: error.errors.map((error) => error.message).join('/n'), 78 74 } 79 - } else if (status === 404) { 80 - return { error: `Repo ${owner}/${repo} not found` } 81 - } else if (status === 403) { 82 - return { error: 'GitHub rate limit exceeded' } 83 - } else if (status !== undefined) { 84 - return { error: [`Couldn't fetch stargazers, code ${status}`, message].join(': ') } 75 + } else if (error instanceof Error) { 76 + return { error: `${error.name}: ${error.message}` } 85 77 } else { 86 78 return { error: "Couldn't fetch stargazers" } 87 79 }