...a "long" format, where each type of fruit is grouped into its own array and each datapoint is a row. The column name becomes a property on the group whose name we define with the zKey variable.
We also need a flat, ungrouped array of objects so that Layer Cake can measure the full data extents. This gets passed to the flatData prop so the scales know the full domain of the data.
<script>import { LayerCake, ScaledSvg, Html, flatten } from'layercake';
import { scaleOrdinal } from'd3-scale';
import { timeParse, timeFormat } from'd3-time-format';
import { format } from'd3-format';
importMultiLinefrom'./_components/MultiLine.svelte';
importAxisXfrom'./_components/AxisX.percent-range.html.svelte';
importAxisYfrom'./_components/AxisY.percent-range.html.svelte';
importGroupLabelsfrom'./_components/GroupLabels.html.svelte';
importSharedTooltipfrom'./_components/SharedTooltip.percent-range.html.svelte';
// This example loads csv data as json using @rollup/plugin-dsvimport data from'./_data/fruit.csv';
/* --------------------------------------------
* Set what is our x key to separate it from the other series
*/const xKey = 'month';
const yKey = 'value';
const zKey = 'fruit';
const seriesNames = Object.keys(data[0]).filter(d => d !== xKey);
const seriesColors = ['#ffe4b8', '#ffb3c0', '#ff7ac7', '#ff00cc'];
const parseDate = timeParse('%Y-%m-%d');
/* --------------------------------------------
* Create a "long" format that is a grouped series of data points
* Layer Cake uses this data structure and the key names
* set in xKey, yKey and zKey to map your data into each scale.
*/const dataLong = seriesNames.map(key => {
return {
[zKey]: key,
values: data.map(d => {
// Put this in a conditional so that we don't recast the data on second render
d[xKey] = typeof d[xKey] === 'string' ? parseDate(d[xKey]) : d[xKey];
return {
[yKey]: +d[key],
[xKey]: d[xKey],
[zKey]: key
};
})
};
});
const formatLabelX = timeFormat('%b. %e');
constformatLabelY = d => format(`~s`)(d);
</script><divclass="chart-container"><LayerCakessrpercentRangepadding={{ top: 7, right: 10, bottom: 20, left: 25 }}x={xKey}y={yKey}z={zKey}zScale={scaleOrdinal()}zRange={seriesColors}flatData={flatten(dataLong, 'values')}yDomain={[0, null]}data={dataLong}
><Html><AxisXgridlines={false}ticks={data.map(d => d[xKey]).sort((a, b) => a - b)}format={formatLabelX}snapLabelstickMarks
/><AxisYformat={formatLabelY} /></Html><ScaledSvg><MultiLine /></ScaledSvg><Html><GroupLabels /><SharedTooltipformatTitle={formatLabelX}dataset={data} /></Html></LayerCake></div><style>/*
The wrapper div needs to have an explicit width and height in CSS.
It can also be a flexbox child or CSS grid element.
The point being it needs dimensions since the <LayerCake> element will
expand to fill it.
*/.chart-container {
width: 100%;
height: 250px;
}
</style>
<!--
@component
Generates an SVG multi-series line chart. It expects your data to be an array of objects, each with a `values` key that is an array of data objects.
--><script>import { getContext } from'svelte';
import { line, curveLinear } from'd3-shape';
const { data, xGet, yGet, zGet } = getContext('LayerCake');
/** @typedef {import('d3-shape').CurveFactory} CurveFactory *//**
* @typedef {Object} Props
* @property {CurveFactory} [curve] - An optional D3 interpolation function. See [d3-shape](https://github.com/d3/d3-shape#curves) for options. Pass this function in uncalled, i.e. without the open-close parentheses.
*//** @type {Props} */let { curve = curveLinear } = $props();
let path = $derived(line().x($xGet).y($yGet).curve(curve));
// .defined($y)</script><gclass="line-group">{#each $data as group}<pathclass="path-line"d={path(group.values)}stroke={$zGet(group)}></path>{/each}</g><style>.path-line {
fill: none;
stroke-linejoin: round;
stroke-linecap: round;
stroke-width: 3px;
}
</style>
<!--
@component
Generates an HTML x-axis, useful for server-side rendered charts. 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.
Although this is marked as a percent-range component, you can also use it with a normal scale with no configuration needed. By default, if you have `percentRange={true}` it will use percentages, otherwise it will use pixels. This makes this component compatible with server-side and client-side rendered charts. Set the `units` prop to either `'%'` or `'px'` to override the default behavior.
--><script>import { getContext } from'svelte';
const { xScale, percentRange } = getContext('LayerCake');
/**
* @typedef {Object} Props
* @property {boolean} [tickMarks=false] - Show a vertical mark for each tick.
* @property {boolean} [gridlines=true] - Show gridlines extending into the chart area.
* @property {number} [tickMarkLength=6] - The length of the tick mark.
* @property {boolean} [baseline=false] - Show a solid line at the bottom.
* @property {boolean} [snapLabels=false] - Instead of centering the text labels on the first and the last items, align them to the edges of the chart.
* @property {(d: any) => string} [format=d => d] - A function that passes the current tick value and expects a nicely formatted value in return.
* @property {number|Array<any>|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.
* @property {number} [tickGutter=0] - The amount of whitespace between the start of the tick and the chart drawing area (the yRange min).
* @property {number} [dx=0] - Any optional value passed to the `dx` attribute on the text label.
* @property {number} [dy=0] - Any optional value passed to the `dy` attribute on the text label.
* @property {'px'|'%'} [units] - If `percentRange={true}` it defaults to `'%'`, otherwise, the default is `'px'`. Options: `'%'` or `'px'`
*//** @type {Props} */let {
tickMarks = false,
gridlines = true,
tickMarkLength = 6,
baseline = false,
snapLabels = false,
format = d => d,
ticks = undefined,
tickGutter = 0,
dx = 0,
dy = 0,
units = $percentRange === true ? '%' : 'px'
} = $props();
let tickLen = $derived(tickMarks === true ? (tickMarkLength ?? 6) : 0);
let isBandwidth = $derived(typeof $xScale.bandwidth === 'function');
/** @type {Array<any>} */let tickVals = $derived(
Array.isArray(ticks)
? ticks
: isBandwidth
? $xScale.domain()
: typeof ticks === 'function'
? ticks($xScale.ticks())
: $xScale.ticks(ticks)
);
let halfBand = $derived(isBandwidth ? $xScale.bandwidth() / 2 : 0);
</script><divclass="axis x-axis"class:snapLabels>{#each tickVals as tick, i (tick)}{@const tickValUnits = $xScale(tick)}{#if baseline === true}<divclass="baseline"style="top:100%; width:100%;"></div>{/if}{#if gridlines === true}<divclass="gridline"style:left="{tickValUnits}{units}"style="top:0; bottom:0;"></div>{/if}{#if tickMarks === true}<divclass="tick-mark"style:left="{tickValUnits + halfBand}{units}"style:height="{tickLen}px"style:bottom="{-tickLen - tickGutter}px"
></div>{/if}<divclass="tick tick-{i}"style:left="{tickValUnits + halfBand}{units}"style="top:calc(100% + {tickGutter}px);"
><divclass="text"style:top="{tickLen}px"style:transform="translate(calc(-50% + {dx}px), {dy}px)"
>{format(tick)}</div></div>{/each}</div><style>.axis,
.tick,
.tick-mark,
.gridline,
.baseline {
position: absolute;
}
.axis {
width: 100%;
height: 100%;
}
.tick {
font-size: 11px;
}
.gridline {
border-left: 1px dashed #aaa;
}
.tick-mark {
border-left: 1px solid #aaa;
}
.baseline {
border-top: 1px solid #aaa;
}
.tick.text {
color: #666;
position: relative;
white-space: nowrap;
transform: translateX(-50%);
}
/* This looks a little better at 40 percent than 50 */.axis.snapLabels.tick:last-child {
transform: translateX(-40%);
}
.axis.snapLabels.tick.tick-0 {
transform: translateX(40%);
}
</style>
<!--
@component
Generates an HTML 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.
Although this is marked as a percent-range component, you can also use it with a normal scale with no configuration needed. By default, if you have `percentRange={true}` it will use percentages, otherwise it will use pixels. This makes this component compatible with server-side and client-side rendered charts. Set the `units` prop to either `'%'` or `'px'` to override the default behavior.
--><script>import { getContext } from'svelte';
const { xRange, yScale, percentRange } = getContext('LayerCake');
/**
* @typedef {Object} Props
* @property {boolean} [tickMarks=false] - Show marks next to the tick label.
* @property {string} [labelPosition='even'] - Whether the label sits even with its value ('even') or sits on top ('above') the tick mark. Default is 'even'.
* @property {boolean} [snapBaselineLabel=false] - When labelPosition='even', adjust the lowest label so that it sits above the tick mark.
* @property {boolean} [gridlines=true] - Show gridlines extending into the chart area.
* @property {number} [tickMarkLength] - The length of the tick mark. If not set, becomes the length of the widest tick.
* @property {(d: any) => string} [format=d => d] - A function that passes the current tick value and expects a nicely formatted value in return.
* @property {number|Array<any>|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.
* @property {number} [tickGutter=0] - The amount of whitespace between the start of the tick and the chart drawing area (the xRange min).
* @property {number} [dx=0] - Any optional value passed to the `dx` attribute on the text label.
* @property {number} [dy=-3] - Any optional value passed to the `dy` attribute on the text label.
* @property {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).
* @property {'px'|'%'} [units] - If `percentRange={true}` it defaults to `'%'`, otherwise, the default is `'px'`. Options: `'%'` or `'px'`
*//** @type {Props} */let {
tickMarks = false,
labelPosition = 'even',
snapBaselineLabel = false,
gridlines = true,
tickMarkLength = undefined,
format = d => d,
ticks = 4,
tickGutter = 0,
dx = 0,
dy = -3,
charPixelWidth = 7.25,
units = $percentRange === true ? '%' : 'px'
} = $props();
/** @param {number} sum
* @param {string} val */functioncalcStringLength(sum, val) {
if (val === ',' || val === '.') return sum + charPixelWidth * 0.5;
return sum + charPixelWidth;
}
let isBandwidth = $derived(typeof $yScale.bandwidth === 'function');
/** @type {Array<any>} */let tickVals = $derived(
Array.isArray(ticks)
? ticks
: isBandwidth
? $yScale.domain()
: typeof ticks === 'function'
? ticks($yScale.ticks())
: $yScale.ticks(ticks)
);
let widestTickLen = $derived(
Math.max(
10,
Math.max(...tickVals.map(d =>format(d).toString().split('').reduce(calcStringLength, 0)))
)
);
let tickLen = $derived(
tickMarks === true
? labelPosition === 'above'
? (tickMarkLength ?? widestTickLen)
: (tickMarkLength ?? 6)
: 0
);
let x1 = $derived(-tickGutter - (labelPosition === 'above' ? widestTickLen : tickLen));
let halfBand = $derived(isBandwidth ? $yScale.bandwidth() / 2 : 0);
let maxTickValUnits = $derived(Math.max(...tickVals.map($yScale)));
</script><divclass="axis y-axis">{#each tickVals as tick, i (tick)}{@const tickValUnits = $yScale(tick)}<divclass="tick tick-{i}"style="left:{$xRange[0]}{units};top:{tickValUnits + halfBand}{units};"
>{#if gridlines === true}<divclass="gridline"style="top:0;"style:left="{x1}px"style:right="0px"></div>{/if}{#if tickMarks === true}<divclass="tick-mark"style:top="0"style:left="{x1}px"style:width="{tickLen}px"></div>{/if}<divclass="text"style:top="0"style:text-align={labelPosition === 'even' ? 'right' : 'left'}style:width="{widestTickLen}px"style:left="{-widestTickLen - tickGutter - (labelPosition === 'even' ? tickLen : 0)}px"style:transform="translate({dx + (labelPosition === 'even' ? -3 : 0)}px, calc(-50% + {dy +
(labelPosition === 'above' ||
(snapBaselineLabel === true && tickValUnits === maxTickValUnits)
? -3
: 4)}px))"
>{format(tick)}</div></div>{/each}</div><style>.axis,
.tick,
.tick-mark,
.gridline,
.baseline,
.text {
position: absolute;
}
.axis {
width: 100%;
height: 100%;
}
.tick {
font-size: 11px;
width: 100%;
}
.gridline {
border-top: 1px dashed #aaa;
}
.tick-mark {
border-top: 1px solid #aaa;
}
.baseline.gridline {
border-top-style: solid;
}
.tick.text {
color: #666;
}
</style>
<!--
@component
Generates HTML text labels for a nested data structure. It places the label near the y-value of the highest x-valued data point. This is useful for labeling the final point in a multi-series line chart, for example. It expects your data to be an array of objects where each has `values` field that is an array of data objects. It uses the `z` field accessor to pull the text label.
--><script>import { getContext } from'svelte';
import { max } from'd3-array';
const { data, x, y, xScale, yScale, xRange, yRange, z } = getContext('LayerCake');
/* --------------------------------------------
* Title case the first letter
*/constcap = val => val.replace(/^\w/, d => d.toUpperCase());
/* --------------------------------------------
* Put the label on the highest value
*/let left = $derived(values => $xScale(max(values, $x)) / Math.max(...$xRange));
let top = $derived(values => $yScale(max(values, $y)) / Math.max(...$yRange));
</script>{#each $data as group}<divclass="label"style="
top:{top(group.values) * 100}%;
left:{left(group.values) * 100}%;
"
>{cap($z(group))}</div>{/each}<style>.label {
position: absolute;
transform: translate(-100%, -100%) translateY(1px);
font-size: 13px;
}
</style>
<!--
@component
Generates a tooltip that works on multiseries datasets, like multiline charts. It creates a tooltip showing the name of the series and the current value. This version uses percentages so you can use it to render server-side. It finds the nearest data point using the [QuadTree.percent-range.html.svelte](https://layercake.graphics/components/QuadTree.percent-range.html.svelte) component.
--><script>import { getContext } from'svelte';
import { format } from'd3-format';
importQuadTreefrom'./QuadTree.percent-range.html.svelte';
const { data, width, yScale, config } = getContext('LayerCake');
const commas = format(',');
consttitleCase = d => d.replace(/^\w/, w => w.toUpperCase());
/**
* @typedef {Object} Props
* @property {Function} [formatTitle=d => d] - A function to format the tooltip title, which is `$config.x`.
* @property {Function} [formatKey = d => titleCase(d)] - A function to format the series name.
* @property {Function} [formatValue = d => (isNaN(+d) ? d : commas(d))] - A function to format the value.
* @property {number} [offset=-20] - A y-offset from the hover point, in pixels.
* @property {Array} [dataset] - The dataset to work off of—defaults to $data if left unset. You can pass something custom in here in case you don't want to use the main data or it's in a strange format.
*//** @type {Props} */let {
formatTitle = d => d,
formatKey = d =>titleCase(d),
formatValue = d => (isNaN(+d) ? d : commas(d)),
offset = -20,
dataset
} = $props();
const w = 150;
const w2 = w / 2;
/* --------------------------------------------
* Sort the keys by the highest value
*/functionsortResult(result) {
if (Object.keys(result).length === 0) return [];
const rows = Object.keys(result)
.filter(d => d !== $config.x)
.map(key => {
return {
key,
value: result[key]
};
})
.sort((a, b) => b.value - a.value);
return rows;
}
</script><QuadTreedataset={dataset || $data}y="x">{#snippet children({ x, y, visible, found, e })}{@const foundSorted = sortResult(found)}{#if visible === true}<divstyle="left:{(x / 100) * $width}px;"class="line"></div><divclass="tooltip"style="
width:{w}px;
display: {visible ? 'block' : 'none'};
top:calc({$yScale(foundSorted[0].value)}% + {offset}px);
left:{Math.min(Math.max(w2, (x / 100) * $width), $width - w2)}px;"
><divclass="title">{formatTitle(found[$config.x])}</div>{#each foundSorted as row}<divclass="row"><spanclass="key">{formatKey(row.key)}:</span>{formatValue(row.value)}</div>{/each}</div>{/if}{/snippet}</QuadTree><style>.tooltip {
position: absolute;
font-size: 13px;
pointer-events: none;
border: 1px solid #ccc;
background: rgba(255, 255, 255, 0.85);
transform: translate(-50%, -100%);
padding: 5px;
z-index: 15;
}
.line {
position: absolute;
top: 0;
bottom: 0;
width: 1px;
border-left: 1px dotted #666;
pointer-events: none;
}
.tooltip,
.line {
transition:
left 250ms ease-out,
top 250ms ease-out;
}
.title {
font-weight: bold;
}
.key {
color: #999;
}
</style>
<!--
@component
Creates an interaction layer (in HTML) using [d3-quadtree](https://github.com/d3/d3-quadtree) to find the nearest datapoint to the mouse. This component creates a snippet that exposes variables `x`, `y`, `found` (the found datapoint), `visible` (a Boolean whether any data was found) and `e` (the event object). This component works with a percent range so the `x` and `y` values coming back will be percentages.
The quadtree searches across both the x and y dimensions at the same time. But if you want to only search across one, set the `x` and `y` props to the same value. For example, the [shared tooltip component](https://layercake.graphics/components/SharedTooltip.html.svelte) sets `y='x'` since it's nicer behavior to only pick up on the nearest x-value.
--><script>import { getContext } from'svelte';
import { quadtree } from'd3-quadtree';
const { data, xGet, yGet, width, height } = getContext('LayerCake');
let visible = $state(false);
let found = $state({});
let e = $state({});
/**
* @typedef {Object} Props
* @property {string} [x='x'] - The dimension to search across when moving the mouse left and right.
* @property {string} [y='y'] - The dimension to search across when moving the mouse up and down.
* @property {number|undefined} [searchRadius] - The number of pixels to search around the mouse's location. This is the third argument passed to [`quadtree.find`](https://github.com/d3/d3-quadtree#quadtree_find) and by default a value of `undefined` means an unlimited range.
* @property {Array<Object>|undefined} [dataset] - The dataset to work off of—defaults to $data if left unset. You can pass something custom in here in case you don't want to use the main data or it's in a strange format.
* @property {import('svelte').Snippet<[any]>} [children]
*//** @type {Props} */let { x = 'x', y = 'y', searchRadius, dataset, children } = $props();
let xGetter = $derived(x === 'x' ? $xGet : $yGet);
let yGetter = $derived(y === 'y' ? $yGet : $xGet);
/** @param {MouseEvent} evt*/functionfindItem(evt) {
e = evt;
const xLayerKey = /** @type {'layerX'|'layerY'} */ (`layer${x.toUpperCase()}`);
const yLayerKey = /** @type {'layerX'|'layerY'}*/ (`layer${y.toUpperCase()}`);
const xLayerVal = (evt[xLayerKey] / (x === 'x' ? $width : $height)) * 100;
const yLayerVal = (evt[yLayerKey] / (y === 'y' ? $height : $width)) * 100;
found = finder.find(xLayerVal, yLayerVal, searchRadius) || {};
visible = Object.keys(found).length > 0;
}
let finder = $derived(
quadtree()
.extent([
[-1, -1],
[$width + 1, $height + 1]
])
.x(xGetter)
.y(yGetter)
.addAll(dataset || $data)
);
</script><divclass="bg"onmousemove={findItem}onmouseout={() => (visible = false)}onblur={() => (visible = false)}role="tooltip"
></div>{@render children?.({ x: xGetter(found) || 0, y: yGetter(found) || 0, found, visible, e })}<style>.bg {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
</style>