buckets
This commit is contained in:
parent
1a196b78e6
commit
500a733cec
|
@ -11,27 +11,23 @@
|
|||
|
||||
// Apex charts directly modifies the options object with default properties and internal variables. These being present could unintentionally cause issues to the provider of this prop as the changes are reflected in that component as well. To prevent any issues we clone options here to provide a buffer.
|
||||
$: optionsCopy = cloneDeep(options);
|
||||
$: console.log(cloneDeep(options));
|
||||
|
||||
let chartElement;
|
||||
let chart;
|
||||
let currentType = null
|
||||
|
||||
const updateChart = async (newOptions) => {
|
||||
console.log('update')
|
||||
// Line charts won't transition from category to datetime types properly without
|
||||
// calling this with an empty object first; I don't know why this works.
|
||||
// Line charts have issues transitioning between "datetime" and "category" types, and will ignore the provided formatters
|
||||
// in certain scenarios. Rerendering the chart when the user changes label type fixes this, but unfortunately it does
|
||||
// cause a little bit of jankiness with animations.
|
||||
if (newOptions?.xaxis?.type && newOptions.xaxis.type !== currentType ) {
|
||||
console.log('calling render')
|
||||
await renderChart(chartElement);
|
||||
|
||||
} else {
|
||||
await chart?.updateOptions(newOptions)
|
||||
}
|
||||
}
|
||||
|
||||
const renderChart = async (newChartElement) => {
|
||||
console.log('render')
|
||||
await chart?.destroy()
|
||||
chart = new ApexCharts(newChartElement, optionsCopy)
|
||||
currentType = optionsCopy?.xaxis?.type
|
||||
|
|
|
@ -1,135 +1,137 @@
|
|||
<script>
|
||||
import { ApexOptionsBuilder } from "./ApexOptionsBuilder"
|
||||
import ApexChart from "./ApexChart.svelte"
|
||||
import { get } from "lodash";
|
||||
import formatters from "./formatters"
|
||||
|
||||
export let title
|
||||
export let dataProvider
|
||||
export let valueColumn
|
||||
export let title
|
||||
export let xAxisLabel
|
||||
export let yAxisLabel
|
||||
export let height
|
||||
export let width
|
||||
export let dataLabels
|
||||
export let animate
|
||||
export let legend
|
||||
export let stacked
|
||||
export let palette
|
||||
export let c1, c2, c3, c4, c5
|
||||
export let horizontal
|
||||
export let bucketCount = 10
|
||||
|
||||
$: options = setUpChart(
|
||||
title,
|
||||
dataProvider,
|
||||
valueColumn,
|
||||
xAxisLabel || valueColumn,
|
||||
yAxisLabel,
|
||||
height,
|
||||
width,
|
||||
dataLabels,
|
||||
animate,
|
||||
palette,
|
||||
horizontal,
|
||||
c1 && c2 && c3 && c4 && c5 ? [c1, c2, c3, c4, c5] : null,
|
||||
customColor,
|
||||
bucketCount
|
||||
)
|
||||
$: series = getSeries(dataProvider, valueColumn, bucketCount)
|
||||
|
||||
$: customColor = palette === "Custom"
|
||||
|
||||
const setUpChart = (
|
||||
title,
|
||||
dataProvider,
|
||||
valueColumn,
|
||||
xAxisLabel, //freqAxisLabel
|
||||
yAxisLabel, //valueAxisLabel
|
||||
height,
|
||||
width,
|
||||
dataLabels,
|
||||
animate,
|
||||
palette,
|
||||
horizontal,
|
||||
colors,
|
||||
customColor,
|
||||
bucketCount
|
||||
) => {
|
||||
const allCols = [valueColumn]
|
||||
if (
|
||||
!dataProvider ||
|
||||
!dataProvider.rows?.length ||
|
||||
allCols.find(x => x == null)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Fetch data
|
||||
const { schema, rows } = dataProvider
|
||||
const reducer = row => (valid, column) => valid && row[column] != null
|
||||
const hasAllColumns = row => allCols.reduce(reducer(row), true)
|
||||
const data = rows.filter(row => hasAllColumns(row)).slice(0, 100)
|
||||
if (!schema || !data.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Initialise default chart
|
||||
let builder = new ApexOptionsBuilder()
|
||||
.type("bar")
|
||||
.title(title)
|
||||
.width(width)
|
||||
.height(height)
|
||||
.xLabel(horizontal ? yAxisLabel : xAxisLabel)
|
||||
.yLabel(horizontal ? xAxisLabel : yAxisLabel)
|
||||
.dataLabels(dataLabels)
|
||||
.animate(animate)
|
||||
.palette(palette)
|
||||
.horizontal(horizontal)
|
||||
.colors(customColor ? colors : null)
|
||||
|
||||
if (horizontal) {
|
||||
builder = builder.setOption(["plotOptions", "bar", "barHeight"], "90%")
|
||||
} else {
|
||||
builder = builder.setOption(["plotOptions", "bar", "columnWidth"], "99%")
|
||||
}
|
||||
|
||||
// Pull occurences of the value.
|
||||
let flatlist = data.map(row => {
|
||||
return row[valueColumn]
|
||||
})
|
||||
|
||||
// Build range buckets
|
||||
let interval = Math.max(...flatlist) / bucketCount
|
||||
let counts = Array(bucketCount).fill(0)
|
||||
|
||||
// Assign row data to a bucket
|
||||
let buckets = flatlist.reduce((acc, val) => {
|
||||
let dest = Math.min(Math.floor(val / interval), bucketCount - 1)
|
||||
acc[dest] = acc[dest] + 1
|
||||
return acc
|
||||
}, counts)
|
||||
|
||||
const rangeLabel = bucketIdx => {
|
||||
return `${Math.floor(interval * bucketIdx)} - ${Math.floor(
|
||||
interval * (bucketIdx + 1)
|
||||
)}`
|
||||
}
|
||||
|
||||
const series = [
|
||||
{
|
||||
name: yAxisLabel,
|
||||
data: Array.from({ length: buckets.length }, (_, i) => ({
|
||||
x: rangeLabel(i),
|
||||
y: buckets[i],
|
||||
})),
|
||||
$: options = {
|
||||
/*
|
||||
series,
|
||||
colors: palette === "Custom" ? [c1, c2, c3, c4, c5] : [],
|
||||
theme: {
|
||||
palette: palette === "Custom" ? null : palette
|
||||
},
|
||||
legend: {
|
||||
show: legend,
|
||||
position: "top",
|
||||
horizontalAlign: "right",
|
||||
showForSingleSeries: true,
|
||||
showForNullSeries: true,
|
||||
showForZeroSeries: true,
|
||||
},
|
||||
title: {
|
||||
text: title,
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: dataLabels
|
||||
},
|
||||
chart: {
|
||||
height: height == null || height === "" ? "auto" : height,
|
||||
width: width == null || width === "" ? "100%" : width,
|
||||
type: "bar",
|
||||
stacked,
|
||||
animations: {
|
||||
enabled: animate
|
||||
},
|
||||
]
|
||||
|
||||
builder = builder.setOption(["xaxis", "labels"], {
|
||||
formatter: x => {
|
||||
return x + ""
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
})
|
||||
zoom: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal
|
||||
}
|
||||
},
|
||||
// We can just always provide the categories to the xaxis and horizontal mode automatically handles "tranposing" the categories to the yaxis, but certain things like labels need to be manually put on a certain axis based on the selected mode. Titles do not need to be handled this way, they are exposed to the user as "X axis" and Y Axis" so flipping them would be confusing.
|
||||
xaxis: {
|
||||
type: labelType,
|
||||
categories,
|
||||
labels: {
|
||||
formatter: xAxisFormatter
|
||||
},
|
||||
title: {
|
||||
text: xAxisLabel
|
||||
}
|
||||
},
|
||||
// Providing `type: "datetime"` normally makes Apex Charts parse epochs nicely with no additonal config, but bar charts in horizontal mode don't have a default setting for parsing the labels of dates, and will just spit out the unix epoch value. It also doesn't seem to respect any date based formatting properties passed in. So we'll just manualy format the labels, the chart still sorts the dates correctly in any case
|
||||
yaxis: {
|
||||
labels: {
|
||||
formatter: yAxisFormatter
|
||||
},
|
||||
title: {
|
||||
text: yAxisLabel
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
builder = builder.series(series)
|
||||
const getSeries = (dataProvider, valueColumn, bucketCount) => {
|
||||
const rows = dataProvider.rows ?? [];
|
||||
|
||||
const values = rows.map(row => parseFloat(row[valueColumn]))
|
||||
const [min, max] = getValuesRange(values)
|
||||
console.log(min, max);
|
||||
const buckets = getBuckets(min, max, bucketCount)
|
||||
}
|
||||
|
||||
const getValuesRange = (values) => {
|
||||
// Ensure min is nearest integer including the actual minimum e.g.`-10.2` -> `-11`
|
||||
const min = Math.floor(Math.min(...values))
|
||||
// Ensure max is nearest integer including the actual maximum e.g. `20.2` -> `21`
|
||||
const max = Math.ceil(Math.max(...values))
|
||||
|
||||
return [min, max]
|
||||
}
|
||||
|
||||
const getBuckets = (min, max, bucketCount) => {
|
||||
// Assure bucketCount is >= 2 and an integer
|
||||
bucketCount = bucketCount < 2 ? 2 : Math.floor(bucketCount)
|
||||
|
||||
const range = max - min
|
||||
// Assure bucketSize is never a decimal value, we'll redistribute any size truncated here later
|
||||
const bucketSize = Math.floor(range / bucketCount)
|
||||
let bucketRemainder = range - (bucketSize * bucketCount)
|
||||
|
||||
const buckets = []
|
||||
|
||||
for (let i = 0; i < bucketCount; i++) {
|
||||
const lastBucketMax = buckets?.[buckets.length - 1]?.max ?? min
|
||||
const remainderPadding = i < bucketRemainder ? 1 : 0
|
||||
|
||||
buckets.push({
|
||||
min: lastBucketMax,
|
||||
max: lastBucketMax + bucketSize + remainderPadding
|
||||
})
|
||||
}
|
||||
|
||||
console.log(range);
|
||||
console.log(bucketSize);
|
||||
console.log(bucketRemainder)
|
||||
console.log(buckets);
|
||||
|
||||
return buckets;
|
||||
|
||||
return builder.getOptions()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
Loading…
Reference in New Issue