Skip to content

Commit

Permalink
feat: add first working draft of chartjs islands (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
mbhrznr committed Jul 13, 2023
1 parent cb15e96 commit d9bad1a
Show file tree
Hide file tree
Showing 8 changed files with 229 additions and 19 deletions.
7 changes: 4 additions & 3 deletions Chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import { chart, type ChartConfiguration } from "./core.ts";
import { type ChartJs } from "./deps.ts";

/** A JSX component which is can be used to server side render a chart inline
/**
* A JSX component which can be used to server side render a chart inline
* within a page.
*
* View {@linkcode ChartConfiguration} for a list of properties that can be set
Expand Down Expand Up @@ -54,7 +55,7 @@ export function Chart<
>(opts: ChartConfiguration<TType, TData, TLabel>) {
if (opts.svgClass) {
// Calculates span with the given svg class to make twind aware of the given class
const _span = <span class={opts.svgClass}></span>;
const _span = <span class={opts.svgClass} />;
}
return <span dangerouslySetInnerHTML={{ __html: chart(opts) }}></span>;
return <span dangerouslySetInnerHTML={{ __html: chart(opts) }} />;
}
69 changes: 62 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
# fresh_charts

A server side rendered charting library for Fresh based on
[Chart.js](https://www.chartjs.org/).
A charting library for Fresh based on [Chart.js](https://www.chartjs.org/),
which supports server and client side rendering.

## Usage

There are two main ways to render a chart. There is the JSX/TSX component
`Chart` which can be used to inline a chart on a page, and the `renderChart()`
function which can be used to respond in a handler with an SVG image.
There are several ways to render a chart.

### Inline chart example
For server side rendering there is the JSX/TSX component `Chart` which can be
used to inline a chart on a page, and the `renderChart()` function which can be
used to respond in a handler with an SVG image.

For client side rendering there is also a JSX/TSX island component `Chart`.

### [SSR] Inline chart example

This provides a chart rendered within the router page itself.

Expand Down Expand Up @@ -54,7 +58,7 @@ export default function Home() {
}
```

### Responding as an image
### [SSR] Responding as an image

This route will provide the landing page of the site, which has an image link to
a route, which will send a request to `/routes/chart.ts` to render the chart.
Expand Down Expand Up @@ -120,6 +124,57 @@ export const handler: Handlers = {
};
```

### [CSR] Inline chart example

This provides a client side rendered and interactive chart island within the
router page itself.

**/islands/chart.tsx**

```tsx
import { Chart as default } from "$fresh_charts/island.tsx";
```

**/routes/index.tsx**

```tsx
import { Head } from "$fresh/runtime.ts";
import { ChartColors } from "$fresh_charts/utils.ts";
import { Chart } from "../islands/chart.tsx";

export default function Home() {
return (
<>
<Head>
<title>Example Chart</title>
</Head>
<div class="p-4 mx-auto max-w-screen-md">
<Chart
type="line"
options={{
scales: { yAxes: [{ ticks: { beginAtZero: true } }] },
}}
data={{
labels: ["1", "2", "3"],
datasets: [{
label: "Sessions",
data: [123, 234, 234],
borderColor: ChartColors.Red,
borderWidth: 1,
}, {
label: "Users",
data: [346, 233, 123],
borderColor: ChartColors.Blue,
borderWidth: 1,
}],
}}
/>
</div>
</>
);
}
```

---

Copyright 2018-2022 the Deno authors. All rights reserved. MIT Licensed.
3 changes: 2 additions & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.2.3",
"std/": "https://deno.land/std@0.159.0/",
"twind": "https://esm.sh/twind@0.16.19",
"twind/": "https://esm.sh/twind@0.16.19/"
"twind/": "https://esm.sh/twind@0.16.19/",
"chart.js": "https://esm.sh/stable/chart.js@4.3.0/auto?target=es2022"
},
"tasks": {
"examples": "deno run -A --watch=examples/static/,examples/routes/,examples/islands examples/dev.ts",
Expand Down
5 changes: 4 additions & 1 deletion examples/fresh.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@

import * as $0 from "./routes/chart.ts";
import * as $1 from "./routes/index.tsx";
import * as $$0 from "./islands/chart.tsx";

const manifest = {
routes: {
"./routes/chart.ts": $0,
"./routes/index.tsx": $1,
},
islands: {},
islands: {
"./islands/chart.tsx": $$0,
},
baseUrl: import.meta.url,
};

Expand Down
Empty file removed examples/islands/.keep
Empty file.
1 change: 1 addition & 0 deletions examples/islands/chart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Chart as default } from "$fresh_charts/island.tsx";
61 changes: 54 additions & 7 deletions examples/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

import { Head } from "$fresh/runtime.ts";
import { Chart } from "$fresh_charts/mod.ts";
import { ChartColors, transparentize } from "$fresh_charts/utils.ts";
import { ChartColors } from "$fresh_charts/utils.ts";
import ChartIsland from "../islands/chart.tsx";
import { months, numbers } from "../utils.ts";

export default function Home() {
const barCfg = { count: 7, min: -100, max: 100 };
const lineCfg = { count: 7, min: -100, max: 100 };
const pieCfg = { count: 5, min: 0, max: 100 };
return (
<>
Expand All @@ -25,7 +27,7 @@ export default function Home() {
<h2 class="text(xl gray-600) font-medium">Examples</h2>
</div>
</div>
<h1 class="text(xl gray-600) font-medium mt-4">Bar Chart - Inline</h1>
<h3 class="text(xl gray-600) font-medium mt-4">Bar Chart - Inline</h3>
<Chart
type="bar"
options={{ devicePixelRatio: 1 }}
Expand All @@ -51,7 +53,7 @@ export default function Home() {
}}
svgClass="w-full"
/>
<h1 class="text(xl gray-600) font-medium mt-4">Pie Chart - Inline</h1>
<h3 class="text(xl gray-600) font-medium mt-4">Pie Chart - Inline</h3>
<Chart
type="pie"
options={{ devicePixelRatio: 1 }}
Expand All @@ -73,17 +75,17 @@ export default function Home() {
}}
svgStyle="width: 100%;"
/>
<h1 class="text(xl gray-600) font-medium mt-4">
<h3 class="text(xl gray-600) font-medium mt-4">
Line Chart - Image Tag
</h1>
</h3>
<img
src="/chart"
class="mx-auto my-4 h-96"
alt="an example chart provided as an image"
/>
<h1 class="text(xl gray-600) font-medium mt-4">
<h3 class="text(xl gray-600) font-medium mt-4">
Polar Area Chart - Inline
</h1>
</h3>
<Chart
type="polarArea"
options={{ devicePixelRatio: 1 }}
Expand All @@ -105,6 +107,51 @@ export default function Home() {
}}
svgClass="w-full"
/>
<h3 class="text(xl gray-600) font-medium mt-4">Bar Chart - Island</h3>
<ChartIsland
type="bar"
options={{ interaction: { mode: "index", intersect: false } }}
data={{
labels: months(barCfg),
datasets: [
{
label: "Dataset 1",
data: numbers(barCfg),
backgroundColor: ChartColors.Red,
},
{
label: "Dataset 2",
data: numbers(barCfg),
backgroundColor: ChartColors.Blue,
},
{
label: "Dataset 3",
data: numbers(barCfg),
backgroundColor: ChartColors.Green,
},
],
}}
/>
<h3 class="text(xl gray-600) font-medium mt-4">Line Chart - Island</h3>
<ChartIsland
type="line"
options={{ interaction: { mode: "index", intersect: false } }}
data={{
labels: months(lineCfg),
datasets: [
{
label: "Dataset 1",
data: numbers(lineCfg),
borderColor: ChartColors.Red,
},
{
label: "Dataset 2",
data: numbers(lineCfg),
borderColor: ChartColors.Blue,
},
],
}}
/>
</div>
</>
);
Expand Down
102 changes: 102 additions & 0 deletions island.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import ChartJS, {
type ChartConfiguration,
type ChartType,
type DefaultDataPoint,
} from "chart.js";
import type { JSX } from "preact";
import { useEffect, useRef } from "preact/hooks";

export type { ChartType, DefaultDataPoint };

export type ChartProps<
Type extends ChartType,
Data = DefaultDataPoint<Type>,
Label = unknown,
> = ChartConfiguration<Type, Data, Label> & {
canvas?: JSX.HTMLAttributes<HTMLCanvasElement>;
};

/**
* A hook which takes in a Chart.js configuration object and returns `canvasRef` and `chartRef`.
*
* `canvasRef` is a reference to the canvas element which the chart is rendered to.
* `chartRef` is a reference to the Chart.js instance.
*/
function useChart<
Type extends ChartType,
Data = DefaultDataPoint<Type>,
Label = unknown,
>(options: ChartConfiguration<Type, Data, Label>) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const chartRef = useRef<ChartJS<Type, Data, Label> | null>(null);

useEffect(() => {
if (canvasRef.current === null) {
throw new Error("canvas is null");
}
if (chartRef.current) {
chartRef.current.destroy();
}

chartRef.current = new ChartJS(
canvasRef.current,
options,
);

return () => {
chartRef.current?.destroy();
};
}, [canvasRef, options]);

return { canvasRef, chartRef };
}

/**
* A JSX component which can be used to client side render a chart inline
* within a page.
*
* View {@linkcode ChartProps} for a list of properties that can be set
* on the component.
*
* ### Example
*
* ```tsx
* import { Chart } from "https://deno.land/x/fresh_charts/island.tsx";
* import { ChartColors } from "https://deno.land/x/fresh_charts/utils.ts";
*
* export default App() {
* return <>
* <h1>Chart Example</h1>
* <Chart
* type="line"
* options={{
* scales: { yAxes: [{ ticks: { beginAtZero: true } }] },
* }}
* data={{
* labels: ["1", "2", "3"],
* datasets: [{
* label: "Sessions",
* data: [123, 234, 234],
* borderColor: ChartColors.Red,
* borderWidth: 1,
* }, {
* label: "Users",
* data: [346, 233, 123],
* borderColor: ChartColors.Blue,
* borderWidth: 1,
* }],
* }}
* />
* <>;
* }
* ```
*/
export function Chart<Type extends ChartType>(props: ChartProps<Type>) {
const { canvasRef, chartRef } = useChart<Type>(props);

useEffect(() => {
chartRef.current?.render();
}, []);

return <canvas ref={canvasRef} />;
}

0 comments on commit d9bad1a

Please sign in to comment.