plouc / nivo

nivo provides a rich set of dataviz components, built on top of the awesome d3 and React libraries
https://nivo.rocks
MIT License
13.05k stars 1.02k forks source link

Custom Legend Line Charts #2342

Open jasonamyers opened 1 year ago

jasonamyers commented 1 year ago

Is your feature request related to a problem? Please describe. I'm struggling to get all the legends in a line chart within the width we want. Is there a way to implement a custom layer to arrange the legend as I'd like?

Describe the solution you'd like I'd love to have the ability to make a way to split the legend into multiple rows etc. I'm not looking for auto layout, auto width, or anything. Or if there is a way to get the legend data/colors so I can use it in an HTML component, that would be awesome as well.

Describe alternatives you've considered I've tried using columns, and I dug into the BoxLegendSvg, but doesn't seem to be anything else available for Line charts

Additional context

image image
plouc commented 1 year ago

Hi @jasonamyers, that's an issue users reported many times 😓, SVG or canvas don't provide much flexibility to build this kind of layout, and I don't want to reimplement this in the lib.

I've started to implement a forwardLegendData property in @nivo/waffle, which will allow users to create their own legends outside of the chart, you can see how it can be used here, but unfortunately, it has not been implemented yet in @nivo/line, and I cannot tell you when it will as I currently have other priorities.

jasonamyers commented 1 year ago

Hey @plouc thank you so much for the response. Completely understand and I really do like what you're doing with the forwardLegendData. Thank you for Nivo and I appreciate all your work and support efforts!

yubi00 commented 12 months ago

@plouc Hey, I was wondering if we could have something similar for the heatmap chart as well. or is thre any way I can add custom legends which could be a react component outside the chart and have access to the main data passed through the heatmap graph? we wanted something similar for our project.

plouc commented 11 months ago

@yubi00, yes, technically, it is feasible.

guillermoscript commented 8 months ago

hey @jasonamyers not sure if this helps but I had the same problem so I create my own custom legends component inspired on how the Pie custom component was made here.

Here you can see the code that I created:

import {
    TooltipProvider,
    Tooltip,
    TooltipTrigger,
    TooltipContent,
} from "@radix-ui/react-tooltip";
import { ResponsiveLine } from '@nivo/line'

interface LineDto {
    id: string;
    data: Daum[];
}
interface Daum {
    x: string;
    y: number;
}

function CustomTooltip({
    data,
}: {
    data: LineDto[];
}) {

    // my own custom colors so its matchs the chart
    const colors = [
        "rgb(31, 119, 180)",
        "rgb(255, 127, 14)",
        "rgb(44, 160, 44)",
        "rgb(214, 39, 40)",
        "rgb(148, 103, 189)",
        "rgb(140, 86, 75)",
        "rgb(227, 119, 194)",
        "rgb(127, 127, 127)",
        "rgb(188, 189, 34)",
        "rgb(23, 190, 207)",
    ];

    return (
        <div
            className={`flex justify-center items-center overflow-auto min-h-[40px] gap-2 px-2 w-full`}
        >
            {data.map((d, i) => {
                const colorIndex = i % colors.length;
                const color = colors[colorIndex];

                // I generate the final string here, you can just use the d.id but in my case the id has a lot of information so i need to format it and thats why i use the generateFinalString function
                console.log(d, 'd')
                const final = generateFinalString(d);

                // I only want to show 6 items in the tooltip, so i check if the index is greater than 6 and if it is i return null
                if (i > 6) {
                    return null;
                }

                return <TooltipItem
                    key={d.id}
                    color={color} final={final} id={d.id} />;
            })}

            {data.length > 6 && (
                <TooltipProvider>
                    <Tooltip>
                        <TooltipTrigger className="text-neutral-600 font-normal text-sm min-w-[90px] ">
                            Next {data.length - 6}
                        </TooltipTrigger>
                        <TooltipContent
                            className={`cursor-pointer p-4 bg-white text-neutral-900 rounded-md shadow-md flex flex-col gap-1`}
                        >
                            {data.map((d, i) => {
                                const colorIndex = i % colors.length;
                                const color = colors[colorIndex];
                                const final = generateFinalString(d);

                                if (i < 6) {
                                    return null;
                                }

                                return <TooltipItem 
                                    key={d.id}
                                    color={color} final={final} id={d.id} />;
                            })}
                        </TooltipContent>
                    </Tooltip>
                </TooltipProvider>
            )}
        </div>
    );
}

export const TooltipItem = ({ color, final, id }: { color: string; final: string; id: string }) => (
    <TooltipProvider key={id}>
        <Tooltip>
            <TooltipTrigger className="flex items-center gap-2 overflow-hidden overflow-ellipsis whitespace-nowrap ">
                <div
                    className="w-3 h-3 min-w-[12px]"
                />
                <p className="text-xs  overflow-hidden overflow-ellipsis whitespace-nowrap">
                    {final}
                </p>
            </TooltipTrigger>
            <TooltipContent

                className={`cursor-pointer p-4 bg-white text-neutral-900 rounded-md shadow-md flex flex-col gap-1`}
            >
                <p>{final}</p>
            </TooltipContent>
        </Tooltip>
    </TooltipProvider>
);

const MyResponsiveLine = ({ data }: {
data: LineDto[]
}) => (
<>
<CustomTooltip
    data={data}
/>
    <ResponsiveLine
        data={data}
        margin={{ top: 50, right: 110, bottom: 50, left: 60 }}
        xScale={{ type: 'point' }}
        yScale={{
            type: 'linear',
            min: 'auto',
            max: 'auto',
            stacked: true,
            reverse: false
        }}
        yFormat=" >-.2f"
        axisTop={null}
        axisRight={null}
        axisBottom={{
            tickSize: 5,
            tickPadding: 5,
            tickRotation: 0,
            legend: 'transportation',
            legendOffset: 36,
            legendPosition: 'middle'
        }}
        axisLeft={{
            tickSize: 5,
            tickPadding: 5,
            tickRotation: 0,
            legend: 'count',
            legendOffset: -40,
            legendPosition: 'middle'
        }}
        pointSize={10}
        pointColor={{ theme: 'background' }}
        pointBorderWidth={2}
        pointBorderColor={{ from: 'serieColor' }}
        pointLabelYOffset={-12}
        useMesh={true}
    />
    </>
)

I use tailwind and radixui for my tooltip component, you could use shadcn or you own custom component, keep in mind the code is for my own requirements but its a blueprint on how you could do your own and with the help of flex you can do exactly the layout you are looking for, hope this helps you.

this is the final result, I needed to add a next tooltip button but you could render all of the content and have it break down in rows with flex-wrap or grid.

Screen Shot 2024-01-02 at 4 37 49 PM

pd: @plouc thanks for this awesome library I really like it :D